From 3ce7d77e2b6d00558cb489051c031023d0ae15ad Mon Sep 17 00:00:00 2001 From: Maximiliano Curia Date: Thu, 24 Jan 2019 12:25:57 +0000 Subject: [PATCH] Import kwin_5.14.5.orig.tar.xz [dgit import orig kwin_5.14.5.orig.tar.xz] --- .arcconfig | 4 + 16-apps-kwin.png | Bin 0 -> 380 bytes 32-apps-kwin.png | Bin 0 -> 611 bytes 48-apps-kwin.png | Bin 0 -> 877 bytes CMakeLists.txt | 725 ++++ COMPLIANCE | 254 ++ CONFIGURING | 73 + COPYING | 346 ++ COPYING.DOC | 397 ++ ExtraDesktop.sh | 4 + HACKING | 5 + KWinDBusInterfaceConfig.cmake.in | 6 + Mainpage.dox | 19 + Messages.sh | 3 + README | 209 + abstract_client.cpp | 1899 +++++++++ abstract_client.h | 1226 ++++++ abstract_opengl_context_attribute_builder.cpp | 40 + abstract_opengl_context_attribute_builder.h | 126 + abstract_output.cpp | 130 + abstract_output.h | 158 + activation.cpp | 893 ++++ activities.cpp | 218 + activities.h | 129 + appmenu.cpp | 131 + appmenu.h | 75 + atoms.cpp | 84 + atoms.h | 91 + autotests/CMakeLists.txt | 445 ++ autotests/abstract_client.h | 1 + autotests/client.h | 1 + autotests/drm/CMakeLists.txt | 26 + autotests/drm/mock_drm.cpp | 78 + autotests/drm/mock_drm.h | 32 + autotests/drm/objecttest.cpp | 218 + autotests/fakeeffectplugin.cpp | 49 + autotests/fakeeffectplugin.json | 13 + autotests/fakeeffectplugin_version.cpp | 50 + autotests/fakeeffectplugin_version.json | 13 + autotests/integration/CMakeLists.txt | 84 + autotests/integration/activities_test.cpp | 162 + .../colorcorrect_nightcolor_test.cpp | 334 ++ .../data/anim-data-delete-effect/effect.js | 25 + autotests/integration/data/example.desktop | 3 + .../data/rules/maximize-vert-apply-initial | 13 + autotests/integration/debug_console_test.cpp | 532 +++ .../integration/decoration_input_test.cpp | 895 ++++ .../integration/desktop_window_x11_test.cpp | 178 + .../dont_crash_aurorae_destroy_deco.cpp | 155 + .../dont_crash_cancel_animation.cpp | 126 + .../dont_crash_cursor_physical_size_empty.cpp | 124 + .../integration/dont_crash_empty_deco.cpp | 121 + autotests/integration/dont_crash_glxgears.cpp | 104 + .../integration/dont_crash_no_border.cpp | 134 + .../dont_crash_useractions_menu.cpp | 117 + autotests/integration/effects/CMakeLists.txt | 8 + autotests/integration/effects/fade_test.cpp | 182 + .../effects/scripted_effects_test.cpp | 355 ++ .../effects/scripts/animationTest.js | 12 + .../effects/scripts/animationTestMulti.js | 24 + .../effects/scripts/effectContext.js | 6 + .../effects/scripts/effectsHandler.js | 16 + .../effects/scripts/screenEdgeTest.js | 3 + .../effects/scripts/screenEdgeTouchTest.js | 3 + .../effects/scripts/shortcutsTest.js | 3 + .../effects/slidingpopups_test.cpp | 371 ++ .../integration/effects/translucency_test.cpp | 249 ++ .../effects/windowgeometry_test.cpp | 99 + .../integration/effects/wobbly_shade_test.cpp | 198 + autotests/integration/fakes/CMakeLists.txt | 2 + .../fakes/org.kde.kdecoration2/CMakeLists.txt | 15 + .../fakedecoration_with_shadows.cpp | 72 + .../fakedecoration_with_shadows.json | 15 + .../integration/generic_scene_opengl_test.cpp | 117 + .../integration/generic_scene_opengl_test.h | 40 + .../integration/globalshortcuts_test.cpp | 382 ++ autotests/integration/helper/CMakeLists.txt | 11 + autotests/integration/helper/copy.cpp | 71 + autotests/integration/helper/kill.cpp | 44 + autotests/integration/helper/paste.cpp | 68 + .../integration/idle_inhibition_test.cpp | 129 + .../integration/input_stacking_order.cpp | 190 + autotests/integration/internal_window.cpp | 677 +++ .../integration/keyboard_layout_test.cpp | 461 ++ .../keymap_creation_failure_test.cpp | 102 + autotests/integration/kwin_wayland_test.cpp | 297 ++ autotests/integration/kwin_wayland_test.h | 208 + autotests/integration/kwinbindings_test.cpp | 267 ++ autotests/integration/lockscreen.cpp | 764 ++++ autotests/integration/maximize_test.cpp | 220 + .../modifier_only_shortcut_test.cpp | 386 ++ .../integration/move_resize_window_test.cpp | 857 ++++ .../integration/no_xdg_runtime_dir_test.cpp | 47 + autotests/integration/plasma_surface_test.cpp | 446 ++ autotests/integration/plasmawindow_test.cpp | 367 ++ autotests/integration/platformcursor.cpp | 71 + .../integration/pointer_constraints_test.cpp | 384 ++ autotests/integration/pointer_input.cpp | 1304 ++++++ autotests/integration/quick_tiling_test.cpp | 904 ++++ .../integration/scene_opengl_es_test.cpp | 30 + .../integration/scene_opengl_shadow_test.cpp | 864 ++++ autotests/integration/scene_opengl_test.cpp | 30 + .../scene_qpainter_shadow_test.cpp | 782 ++++ autotests/integration/scene_qpainter_test.cpp | 395 ++ autotests/integration/screen_changes_test.cpp | 195 + .../screenedge_client_show_test.cpp | 296 ++ .../integration/scripting/CMakeLists.txt | 1 + .../integration/scripting/screenedge_test.cpp | 298 ++ .../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 | 143 + .../integration/shell_client_rules_test.cpp | 454 ++ autotests/integration/shell_client_test.cpp | 1020 +++++ .../integration/showing_desktop_test.cpp | 128 + autotests/integration/start_test.cpp | 163 + autotests/integration/struts_test.cpp | 962 +++++ autotests/integration/tabbox_test.cpp | 256 ++ autotests/integration/test_helpers.cpp | 555 +++ autotests/integration/touch_input_test.cpp | 260 ++ .../integration/transient_no_input_test.cpp | 116 + autotests/integration/transient_placement.cpp | 225 + .../integration/virtual_desktop_test.cpp | 169 + autotests/integration/window_rules_test.cpp | 171 + .../integration/window_selection_test.cpp | 559 +++ autotests/integration/x11_client_test.cpp | 605 +++ autotests/integration/xclipboardsync_test.cpp | 183 + autotests/integration/xwayland_input_test.cpp | 212 + autotests/libinput/CMakeLists.txt | 113 + autotests/libinput/context_test.cpp | 92 + autotests/libinput/device_test.cpp | 2198 ++++++++++ autotests/libinput/gesture_event_test.cpp | 214 + autotests/libinput/input_event_test.cpp | 184 + autotests/libinput/key_event_test.cpp | 116 + autotests/libinput/mock_libinput.cpp | 858 ++++ autotests/libinput/mock_libinput.h | 159 + autotests/libinput/mock_udev.cpp | 37 + autotests/libinput/mock_udev.h | 28 + autotests/libinput/pointer_event_test.cpp | 209 + autotests/libinput/switch_event_test.cpp | 99 + autotests/libinput/touch_event_test.cpp | 145 + 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-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-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/kwinglplatformtest.cpp | 284 ++ autotests/libkwineffects/mock_gl.cpp | 70 + autotests/libkwineffects/mock_gl.h | 39 + autotests/libkwineffects/timelinetest.cpp | 256 ++ .../libkwineffects/windowquadlisttest.cpp | 221 + autotests/libxrenderutils/CMakeLists.txt | 13 + .../libxrenderutils/blendpicture_test.cpp | 61 + autotests/mock_abstract_client.cpp | 117 + autotests/mock_abstract_client.h | 70 + autotests/mock_client.cpp | 38 + autotests/mock_client.h | 43 + autotests/mock_effectshandler.cpp | 38 + autotests/mock_effectshandler.h | 269 ++ autotests/mock_screens.cpp | 98 + autotests/mock_screens.h | 53 + autotests/mock_workspace.cpp | 90 + autotests/mock_workspace.h | 83 + autotests/onscreennotificationtest.cpp | 141 + autotests/onscreennotificationtest.h | 38 + .../opengl_context_attribute_builder_test.cpp | 442 ++ autotests/tabbox/CMakeLists.txt | 94 + autotests/tabbox/mock_tabboxclient.cpp | 38 + autotests/tabbox/mock_tabboxclient.h | 73 + autotests/tabbox/mock_tabboxhandler.cpp | 122 + autotests/tabbox/mock_tabboxhandler.h | 113 + autotests/tabbox/test_desktopchain.cpp | 263 ++ autotests/tabbox/test_tabbox_clientmodel.cpp | 94 + autotests/tabbox/test_tabbox_clientmodel.h | 53 + autotests/tabbox/test_tabbox_config.cpp | 85 + autotests/tabbox/test_tabbox_handler.cpp | 63 + autotests/test_builtin_effectloader.cpp | 580 +++ autotests/test_client_machine.cpp | 157 + autotests/test_gbm_surface.cpp | 119 + autotests/test_gestures.cpp | 615 +++ autotests/test_plugin_effectloader.cpp | 419 ++ autotests/test_screen_edges.cpp | 1087 +++++ autotests/test_screen_paint_data.cpp | 287 ++ autotests/test_screens.cpp | 366 ++ autotests/test_scripted_effectloader.cpp | 450 ++ autotests/test_virtual_desktops.cpp | 656 +++ autotests/test_virtualkeyboard_dbus.cpp | 142 + autotests/test_window_paint_data.cpp | 333 ++ autotests/test_x11_timestamp_update.cpp | 126 + autotests/test_xcb_size_hints.cpp | 377 ++ autotests/test_xcb_window.cpp | 213 + autotests/test_xcb_wrapper.cpp | 531 +++ autotests/test_xkb.cpp | 513 +++ autotests/test_xrandr_screens.cpp | 287 ++ autotests/testutils.h | 63 + autotests/workspace.h | 1 + client.cpp | 2100 ++++++++++ client.h | 700 ++++ client_machine.cpp | 242 ++ client_machine.h | 117 + cmake/modules/COPYING-CMAKE-SCRIPTS | 22 + cmake/modules/FindFontconfig.cmake | 50 + cmake/modules/FindLibcap.cmake | 59 + cmake/modules/FindLibdrm.cmake | 126 + cmake/modules/FindLibinput.cmake | 125 + .../FindQt5EventDispatcherSupport.cmake | 122 + .../modules/FindQt5FontDatabaseSupport.cmake | 122 + cmake/modules/FindQt5PlatformSupport.cmake | 121 + cmake/modules/FindQt5ThemeSupport.cmake | 122 + cmake/modules/FindUDev.cmake | 50 + cmake/modules/FindXKB.cmake | 101 + cmake/modules/FindXwayland.cmake | 34 + cmake/modules/Findepoxy.cmake | 56 + cmake/modules/Findgbm.cmake | 125 + cmake/modules/Findlibhybris.cmake | 185 + colorcorrection/colorcorrect_settings.kcfg | 56 + colorcorrection/colorcorrect_settings.kcfgc | 8 + colorcorrection/colorcorrectdbusinterface.cpp | 54 + colorcorrection/colorcorrectdbusinterface.h | 126 + colorcorrection/constants.h | 288 ++ colorcorrection/gammaramp.h | 50 + colorcorrection/manager.cpp | 779 ++++ colorcorrection/manager.h | 147 + colorcorrection/suncalc.cpp | 163 + colorcorrection/suncalc.h | 47 + composite.cpp | 1206 ++++++ composite.h | 240 ++ config-kwin.h.cmake | 42 + cursor.cpp | 477 +++ cursor.h | 303 ++ data/CMakeLists.txt | 11 + data/org_kde_kwin.categories | 21 + data/update_default_rules.cpp | 69 + dbusinterface.cpp | 318 ++ dbusinterface.h | 174 + debug_console.cpp | 1525 +++++++ debug_console.h | 184 + debug_console.ui | 432 ++ decorations/decoratedclient.cpp | 336 ++ decorations/decoratedclient.h | 124 + decorations/decorationbridge.cpp | 301 ++ decorations/decorationbridge.h | 87 + decorations/decorationpalette.cpp | 138 + decorations/decorationpalette.h | 70 + decorations/decorationrenderer.cpp | 88 + decorations/decorationrenderer.h | 86 + decorations/decorations_logging.cpp | 21 + decorations/decorations_logging.h | 26 + decorations/settings.cpp | 191 + decorations/settings.h | 66 + deleted.cpp | 211 + deleted.h | 154 + doc/CMakeLists.txt | 7 + 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 -> 483 bytes doc/kwineffects/configure-filter.png | Bin 0 -> 402 bytes doc/kwineffects/dialog-information.png | Bin 0 -> 619 bytes doc/kwineffects/index.docbook | 86 + doc/kwineffects/video.png | Bin 0 -> 400 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 | 686 +++ 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 | 558 +++ effectloader.h | 379 ++ effects.cpp | 2069 +++++++++ effects.h | 563 +++ effects/CMakeLists.txt | 194 + effects/Messages.sh | 4 + effects/backgroundcontrast/.directory | 3 + effects/backgroundcontrast/CMakeLists.txt | 7 + .../backgroundcontrast.kdev4 | 3 + effects/backgroundcontrast/contrast.cpp | 486 +++ effects/backgroundcontrast/contrast.h | 104 + effects/backgroundcontrast/contrastshader.cpp | 210 + effects/backgroundcontrast/contrastshader.h | 76 + effects/blur/CMakeLists.txt | 24 + effects/blur/blur.cpp | 771 ++++ effects/blur/blur.h | 148 + effects/blur/blur.kcfg | 15 + effects/blur/blur_config.cpp | 62 + effects/blur/blur_config.desktop | 89 + effects/blur/blur_config.h | 46 + effects/blur/blur_config.ui | 160 + effects/blur/blurconfig.kcfgc | 5 + effects/blur/blurshader.cpp | 445 ++ effects/blur/blurshader.h | 116 + effects/colorpicker/colorpicker.cpp | 131 + effects/colorpicker/colorpicker.h | 65 + effects/coverswitch/CMakeLists.txt | 27 + effects/coverswitch/coverswitch.cpp | 1000 +++++ effects/coverswitch/coverswitch.h | 167 + effects/coverswitch/coverswitch.kcfg | 42 + effects/coverswitch/coverswitch_config.cpp | 67 + .../coverswitch/coverswitch_config.desktop | 76 + effects/coverswitch/coverswitch_config.h | 54 + 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 | 54 + effects/cube/cube.cpp | 1740 ++++++++ effects/cube/cube.h | 263 ++ effects/cube/cube.kcfg | 68 + effects/cube/cube_config.cpp | 121 + effects/cube/cube_config.desktop | 83 + effects/cube/cube_config.h | 57 + effects/cube/cube_config.ui | 569 +++ effects/cube/cube_inside.h | 40 + effects/cube/cube_proxy.cpp | 47 + effects/cube/cube_proxy.h | 45 + effects/cube/cubeconfig.kcfgc | 6 + effects/cube/cubeslide.cpp | 609 +++ effects/cube/cubeslide.h | 108 + effects/cube/cubeslide.kcfg | 25 + effects/cube/cubeslide_config.cpp | 69 + effects/cube/cubeslide_config.desktop | 74 + effects/cube/cubeslide_config.h | 54 + effects/cube/cubeslide_config.ui | 105 + effects/cube/cubeslideconfig.kcfgc | 5 + 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 | 46 + effects/cube/data/1.10/sphere.vert | 52 + 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 | 47 + effects/cube/data/1.40/sphere.vert | 53 + effects/cube/data/cubecap.png | Bin 0 -> 305666 bytes effects/desktopgrid/CMakeLists.txt | 35 + effects/desktopgrid/desktopgrid.cpp | 1494 +++++++ effects/desktopgrid/desktopgrid.h | 192 + effects/desktopgrid/desktopgrid.kcfg | 32 + effects/desktopgrid/desktopgrid_config.cpp | 140 + .../desktopgrid/desktopgrid_config.desktop | 85 + effects/desktopgrid/desktopgrid_config.h | 62 + effects/desktopgrid/desktopgrid_config.ui | 250 ++ effects/desktopgrid/desktopgridconfig.kcfgc | 5 + effects/desktopgrid/main.qml | 49 + effects/dialogparent/CMakeLists.txt | 1 + effects/dialogparent/package/CMakeLists.txt | 6 + .../package/contents/code/main.js | 142 + effects/dialogparent/package/metadata.desktop | 159 + effects/diminactive/CMakeLists.txt | 24 + effects/diminactive/diminactive.cpp | 398 ++ effects/diminactive/diminactive.h | 130 + effects/diminactive/diminactive.kcfg | 24 + effects/diminactive/diminactive_config.cpp | 65 + .../diminactive/diminactive_config.desktop | 81 + effects/diminactive/diminactive_config.h | 48 + effects/diminactive/diminactive_config.ui | 76 + effects/diminactive/diminactiveconfig.kcfgc | 5 + effects/dimscreen/CMakeLists.txt | 7 + effects/dimscreen/dimscreen.cpp | 119 + effects/dimscreen/dimscreen.h | 57 + effects/effect_builtins.cpp | 807 ++++ effects/effect_builtins.h | 110 + effects/eyeonscreen/CMakeLists.txt | 1 + effects/eyeonscreen/package/CMakeLists.txt | 6 + .../eyeonscreen/package/contents/code/main.js | 165 + effects/eyeonscreen/package/metadata.desktop | 95 + effects/fade/CMakeLists.txt | 6 + effects/fade/package/contents/code/main.js | 101 + effects/fade/package/contents/config/main.xml | 20 + effects/fade/package/metadata.desktop | 162 + effects/fadedesktop/CMakeLists.txt | 6 + .../fadedesktop/package/contents/code/main.js | 48 + effects/fadedesktop/package/metadata.desktop | 148 + effects/fallapart/CMakeLists.txt | 7 + effects/fallapart/fallapart.cpp | 197 + effects/fallapart/fallapart.h | 67 + effects/fallapart/fallapart.kcfg | 14 + effects/fallapart/fallapartconfig.kcfgc | 5 + effects/flipswitch/CMakeLists.txt | 25 + effects/flipswitch/flipswitch.cpp | 988 +++++ effects/flipswitch/flipswitch.h | 165 + effects/flipswitch/flipswitch.kcfg | 30 + effects/flipswitch/flipswitch_config.cpp | 97 + effects/flipswitch/flipswitch_config.desktop | 77 + effects/flipswitch/flipswitch_config.h | 57 + effects/flipswitch/flipswitch_config.ui | 238 ++ effects/flipswitch/flipswitchconfig.kcfgc | 5 + effects/frozenapp/CMakeLists.txt | 1 + effects/frozenapp/package/CMakeLists.txt | 6 + .../frozenapp/package/contents/code/main.js | 140 + effects/frozenapp/package/metadata.desktop | 90 + effects/glide/CMakeLists.txt | 25 + effects/glide/glide.cpp | 324 ++ effects/glide/glide.h | 156 + effects/glide/glide.kcfg | 40 + effects/glide/glide_config.cpp | 61 + effects/glide/glide_config.desktop | 68 + effects/glide/glide_config.h | 47 + effects/glide/glide_config.ui | 260 ++ effects/glide/glideconfig.kcfgc | 5 + effects/highlightwindow/CMakeLists.txt | 7 + effects/highlightwindow/highlightwindow.cpp | 309 ++ effects/highlightwindow/highlightwindow.h | 87 + effects/invert/CMakeLists.txt | 26 + effects/invert/data/1.10/invert.frag | 22 + effects/invert/data/1.40/invert.frag | 25 + effects/invert/invert.cpp | 151 + effects/invert/invert.h | 75 + effects/invert/invert_config.cpp | 107 + effects/invert/invert_config.desktop | 90 + effects/invert/invert_config.h | 49 + effects/kscreen/CMakeLists.txt | 10 + effects/kscreen/kscreen.cpp | 192 + effects/kscreen/kscreen.h | 66 + effects/kscreen/kscreen.kcfg | 12 + effects/kscreen/kscreenconfig.kcfgc | 5 + effects/kwineffect.desktop | 107 + effects/logging.cpp | 21 + effects/login/CMakeLists.txt | 1 + effects/login/package/CMakeLists.txt | 6 + effects/login/package/contents/code/main.js | 94 + .../login/package/contents/config/main.xml | 12 + effects/login/package/contents/ui/config.ui | 38 + effects/login/package/metadata.desktop | 171 + effects/logout/CMakeLists.txt | 1 + effects/logout/package/CMakeLists.txt | 6 + effects/logout/package/contents/code/main.js | 87 + effects/logout/package/metadata.desktop | 77 + effects/lookingglass/CMakeLists.txt | 28 + .../lookingglass/data/1.10/lookingglass.frag | 25 + .../lookingglass/data/1.40/lookingglass.frag | 28 + effects/lookingglass/lookingglass.cpp | 257 ++ effects/lookingglass/lookingglass.h | 83 + effects/lookingglass/lookingglass.kcfg | 12 + effects/lookingglass/lookingglass_config.cpp | 119 + .../lookingglass/lookingglass_config.desktop | 83 + effects/lookingglass/lookingglass_config.h | 57 + effects/lookingglass/lookingglass_config.ui | 59 + effects/lookingglass/lookingglassconfig.kcfgc | 5 + effects/magiclamp/CMakeLists.txt | 24 + effects/magiclamp/magiclamp.cpp | 374 ++ effects/magiclamp/magiclamp.h | 69 + effects/magiclamp/magiclamp.kcfg | 12 + effects/magiclamp/magiclamp_config.cpp | 71 + effects/magiclamp/magiclamp_config.desktop | 79 + effects/magiclamp/magiclamp_config.h | 54 + effects/magiclamp/magiclamp_config.ui | 53 + effects/magiclamp/magiclampconfig.kcfgc | 5 + effects/magnifier/CMakeLists.txt | 25 + effects/magnifier/magnifier.cpp | 342 ++ effects/magnifier/magnifier.h | 82 + effects/magnifier/magnifier.kcfg | 18 + effects/magnifier/magnifier_config.cpp | 120 + effects/magnifier/magnifier_config.desktop | 90 + effects/magnifier/magnifier_config.h | 57 + effects/magnifier/magnifier_config.ui | 106 + effects/magnifier/magnifierconfig.kcfgc | 5 + effects/maximize/CMakeLists.txt | 1 + effects/maximize/package/CMakeLists.txt | 6 + .../package/contents/code/maximize.js | 103 + effects/maximize/package/metadata.desktop | 113 + effects/minimizeanimation/CMakeLists.txt | 7 + .../minimizeanimation/minimizeanimation.cpp | 165 + effects/minimizeanimation/minimizeanimation.h | 66 + effects/morphingpopups/CMakeLists.txt | 1 + effects/morphingpopups/package/CMakeLists.txt | 6 + .../package/contents/code/morphingpopups.js | 140 + .../morphingpopups/package/metadata.desktop | 90 + effects/mouseclick/CMakeLists.txt | 26 + effects/mouseclick/mouseclick.cpp | 391 ++ effects/mouseclick/mouseclick.h | 185 + effects/mouseclick/mouseclick.kcfg | 34 + effects/mouseclick/mouseclick_config.cpp | 94 + effects/mouseclick/mouseclick_config.desktop | 55 + effects/mouseclick/mouseclick_config.h | 56 + effects/mouseclick/mouseclick_config.ui | 282 ++ effects/mouseclick/mouseclickconfig.kcfgc | 5 + effects/mousemark/CMakeLists.txt | 26 + effects/mousemark/mousemark.cpp | 295 ++ effects/mousemark/mousemark.h | 76 + effects/mousemark/mousemark.kcfg | 15 + effects/mousemark/mousemark_config.cpp | 108 + effects/mousemark/mousemark_config.desktop | 85 + effects/mousemark/mousemark_config.h | 56 + effects/mousemark/mousemark_config.ui | 110 + effects/mousemark/mousemarkconfig.kcfgc | 5 + effects/presentwindows/CMakeLists.txt | 33 + effects/presentwindows/main.qml | 33 + effects/presentwindows/presentwindows.cpp | 2067 +++++++++ effects/presentwindows/presentwindows.h | 351 ++ effects/presentwindows/presentwindows.kcfg | 59 + .../presentwindows/presentwindows_config.cpp | 118 + .../presentwindows_config.desktop | 85 + .../presentwindows/presentwindows_config.h | 57 + .../presentwindows/presentwindows_config.ui | 482 +++ .../presentwindows/presentwindows_proxy.cpp | 47 + effects/presentwindows/presentwindows_proxy.h | 46 + .../presentwindows/presentwindowsconfig.kcfgc | 6 + effects/resize/CMakeLists.txt | 24 + effects/resize/resize.cpp | 177 + effects/resize/resize.h | 73 + effects/resize/resize.kcfg | 15 + effects/resize/resize_config.cpp | 70 + effects/resize/resize_config.desktop | 75 + effects/resize/resize_config.h | 54 + effects/resize/resize_config.ui | 45 + effects/resize/resizeconfig.kcfgc | 5 + effects/scale/CMakeLists.txt | 25 + effects/scale/scale.cpp | 285 ++ effects/scale/scale.h | 116 + effects/scale/scale.kcfg | 28 + effects/scale/scale_config.cpp | 62 + effects/scale/scale_config.desktop | 36 + effects/scale/scale_config.h | 48 + effects/scale/scale_config.ui | 93 + effects/scale/scaleconfig.kcfgc | 5 + effects/screenedge/CMakeLists.txt | 7 + effects/screenedge/screenedgeeffect.cpp | 361 ++ effects/screenedge/screenedgeeffect.h | 79 + effects/screenshot/CMakeLists.txt | 7 + effects/screenshot/screenshot.cpp | 655 +++ effects/screenshot/screenshot.h | 173 + effects/shaders.qrc | 22 + effects/sheet/CMakeLists.txt | 8 + effects/sheet/sheet.cpp | 228 + effects/sheet/sheet.h | 85 + effects/sheet/sheet.kcfg | 12 + effects/sheet/sheetconfig.kcfgc | 5 + effects/showfps/CMakeLists.txt | 25 + effects/showfps/showfps.cpp | 547 +++ effects/showfps/showfps.h | 109 + effects/showfps/showfps.kcfg | 28 + effects/showfps/showfps_config.cpp | 67 + effects/showfps/showfps_config.desktop | 86 + effects/showfps/showfps_config.h | 47 + effects/showfps/showfps_config.ui | 175 + effects/showfps/showfpsconfig.kcfgc | 5 + effects/showpaint/CMakeLists.txt | 7 + effects/showpaint/showpaint.cpp | 126 + effects/showpaint/showpaint.h | 48 + effects/slide/CMakeLists.txt | 24 + effects/slide/slide.cpp | 545 +++ effects/slide/slide.h | 149 + effects/slide/slide.kcfg | 25 + effects/slide/slide_config.cpp | 63 + effects/slide/slide_config.desktop | 73 + effects/slide/slide_config.h | 47 + effects/slide/slide_config.ui | 133 + effects/slide/slideconfig.kcfgc | 5 + effects/slideback/CMakeLists.txt | 7 + effects/slideback/slideback.cpp | 347 ++ effects/slideback/slideback.h | 80 + effects/slidingpopups/CMakeLists.txt | 7 + effects/slidingpopups/slidingpopups.cpp | 464 ++ effects/slidingpopups/slidingpopups.h | 115 + effects/slidingpopups/slidingpopups.kcfg | 17 + .../slidingpopups/slidingpopupsconfig.kcfgc | 5 + effects/snaphelper/CMakeLists.txt | 7 + effects/snaphelper/snaphelper.cpp | 237 ++ effects/snaphelper/snaphelper.h | 59 + effects/startupfeedback/CMakeLists.txt | 10 + .../data/blinking-startup-fragment.glsl | 13 + effects/startupfeedback/startupfeedback.cpp | 402 ++ effects/startupfeedback/startupfeedback.h | 93 + effects/thumbnailaside/CMakeLists.txt | 25 + effects/thumbnailaside/thumbnailaside.cpp | 192 + effects/thumbnailaside/thumbnailaside.h | 91 + effects/thumbnailaside/thumbnailaside.kcfg | 21 + .../thumbnailaside/thumbnailaside_config.cpp | 101 + .../thumbnailaside_config.desktop | 83 + .../thumbnailaside/thumbnailaside_config.h | 56 + .../thumbnailaside/thumbnailaside_config.ui | 138 + .../thumbnailaside/thumbnailasideconfig.kcfgc | 5 + effects/touchpoints/touchpoints.cpp | 325 ++ effects/touchpoints/touchpoints.h | 99 + effects/trackmouse/CMakeLists.txt | 33 + effects/trackmouse/data/tm_inner.png | Bin 0 -> 1247 bytes effects/trackmouse/data/tm_outer.png | Bin 0 -> 1311 bytes effects/trackmouse/trackmouse.cpp | 313 ++ effects/trackmouse/trackmouse.h | 87 + effects/trackmouse/trackmouse.kcfg | 21 + effects/trackmouse/trackmouse_config.cpp | 124 + effects/trackmouse/trackmouse_config.desktop | 86 + effects/trackmouse/trackmouse_config.h | 61 + effects/trackmouse/trackmouse_config.ui | 104 + effects/trackmouse/trackmouseconfig.kcfgc | 5 + effects/translucency/CMakeLists.txt | 1 + effects/translucency/package/CMakeLists.txt | 6 + .../package/contents/code/main.js | 235 ++ .../package/contents/config/main.xml | 36 + .../package/contents/ui/config.ui | 473 +++ effects/translucency/package/metadata.desktop | 172 + effects/windowaperture/CMakeLists.txt | 1 + effects/windowaperture/package/CMakeLists.txt | 6 + .../package/contents/code/main.js | 204 + .../windowaperture/package/metadata.desktop | 95 + effects/windowgeometry/CMakeLists.txt | 25 + effects/windowgeometry/windowgeometry.cpp | 237 ++ effects/windowgeometry/windowgeometry.h | 73 + effects/windowgeometry/windowgeometry.kcfg | 15 + .../windowgeometry/windowgeometry_config.cpp | 95 + .../windowgeometry_config.desktop | 63 + .../windowgeometry/windowgeometry_config.h | 57 + .../windowgeometry/windowgeometry_config.ui | 47 + .../windowgeometry/windowgeometryconfig.kcfgc | 5 + effects/wobblywindows/CMakeLists.txt | 24 + effects/wobblywindows/wobblywindows.cpp | 1245 ++++++ effects/wobblywindows/wobblywindows.h | 219 + effects/wobblywindows/wobblywindows.kcfg | 66 + .../wobblywindows/wobblywindows_config.cpp | 120 + .../wobblywindows_config.desktop | 81 + effects/wobblywindows/wobblywindows_config.h | 52 + effects/wobblywindows/wobblywindows_config.ui | 373 ++ .../wobblywindows/wobblywindowsconfig.kcfgc | 5 + effects/zoom/CMakeLists.txt | 25 + effects/zoom/zoom.cpp | 535 +++ effects/zoom/zoom.h | 134 + effects/zoom/zoom.kcfg | 33 + effects/zoom/zoom_config.cpp | 150 + effects/zoom/zoom_config.desktop | 92 + effects/zoom/zoom_config.h | 57 + effects/zoom/zoom_config.ui | 215 + effects/zoom/zoomconfig.kcfgc | 5 + egl_context_attribute_builder.cpp | 82 + egl_context_attribute_builder.h | 39 + events.cpp | 1345 ++++++ fixqopengl.h | 37 + focuschain.cpp | 269 ++ focuschain.h | 254 ++ geometry.cpp | 3539 ++++++++++++++++ geometrytip.cpp | 66 + geometrytip.h | 44 + gestures.cpp | 210 + gestures.h | 221 + globalshortcuts.cpp | 312 ++ globalshortcuts.h | 192 + group.cpp | 923 ++++ group.h | 99 + helpers/CMakeLists.txt | 2 + helpers/killer/CMakeLists.txt | 16 + helpers/killer/killer.cpp | 131 + helpers/xclipboardsync/CMakeLists.txt | 5 + helpers/xclipboardsync/main.cpp | 47 + helpers/xclipboardsync/waylandclipboard.cpp | 176 + helpers/xclipboardsync/waylandclipboard.h | 56 + idle_inhibition.cpp | 89 + idle_inhibition.h | 66 + input.cpp | 2188 ++++++++++ input.h | 423 ++ input_event.cpp | 63 + input_event.h | 156 + input_event_spy.cpp | 124 + input_event_spy.h | 92 + kcmkwin/CMakeLists.txt | 13 + kcmkwin/kwincompositing/CMakeLists.txt | 87 + kcmkwin/kwincompositing/Messages.sh | 3 + kcmkwin/kwincompositing/compositing.cpp | 532 +++ kcmkwin/kwincompositing/compositing.h | 181 + kcmkwin/kwincompositing/compositing.ui | 296 ++ .../kwincompositing/config-compiler.h.cmake | 7 + kcmkwin/kwincompositing/config-prefix.h.cmake | 34 + kcmkwin/kwincompositing/effectconfig.cpp | 115 + kcmkwin/kwincompositing/effectconfig.h | 47 + .../kwincompositing/kcmkwineffects.desktop | 130 + .../kwincompositing/kwincompositing.desktop | 137 + kcmkwin/kwincompositing/kwineffect.knsrc | 46 + kcmkwin/kwincompositing/main.cpp | 267 ++ kcmkwin/kwincompositing/model.cpp | 661 +++ kcmkwin/kwincompositing/model.h | 201 + kcmkwin/kwincompositing/qml/Effect.qml | 170 + kcmkwin/kwincompositing/qml/EffectView.qml | 177 + kcmkwin/kwincompositing/qml/Video.qml | 72 + kcmkwin/kwincompositing/qml/main.qml | 31 + .../kwincompositing/test/effectmodeltest.cpp | 43 + .../kwincompositing/test/effectmodeltest.h | 38 + kcmkwin/kwincompositing/test/modeltest.cpp | 600 +++ kcmkwin/kwincompositing/test/modeltest.h | 96 + kcmkwin/kwindecoration/CMakeLists.txt | 41 + kcmkwin/kwindecoration/Messages.sh | 4 + .../declarative-plugin/CMakeLists.txt | 26 + .../declarative-plugin/buttonsmodel.cpp | 183 + .../declarative-plugin/buttonsmodel.h | 64 + .../declarative-plugin/plugin.cpp | 55 + .../declarative-plugin/plugin.h | 41 + .../declarative-plugin/previewbridge.cpp | 255 ++ .../declarative-plugin/previewbridge.h | 139 + .../declarative-plugin/previewbutton.cpp | 139 + .../declarative-plugin/previewbutton.h | 81 + .../declarative-plugin/previewclient.cpp | 465 ++ .../declarative-plugin/previewclient.h | 214 + .../declarative-plugin/previewitem.cpp | 500 +++ .../declarative-plugin/previewitem.h | 104 + .../declarative-plugin/previewsettings.cpp | 279 ++ .../declarative-plugin/previewsettings.h | 163 + .../kwindecoration/declarative-plugin/qmldir | 2 + kcmkwin/kwindecoration/decorationmodel.cpp | 196 + kcmkwin/kwindecoration/decorationmodel.h | 65 + kcmkwin/kwindecoration/kcm.cpp | 435 ++ kcmkwin/kwindecoration/kcm.h | 80 + kcmkwin/kwindecoration/kcm.ui | 182 + kcmkwin/kwindecoration/kwindecoration.desktop | 165 + kcmkwin/kwindecoration/qml/ButtonGroup.qml | 75 + kcmkwin/kwindecoration/qml/Buttons.qml | 228 + kcmkwin/kwindecoration/qml/Previews.qml | 151 + kcmkwin/kwindecoration/qml/main.qml | 37 + kcmkwin/kwindesktop/CMakeLists.txt | 33 + kcmkwin/kwindesktop/Messages.sh | 4 + kcmkwin/kwindesktop/desktop.desktop | 161 + kcmkwin/kwindesktop/desktopnameswidget.cpp | 124 + kcmkwin/kwindesktop/desktopnameswidget.h | 63 + kcmkwin/kwindesktop/main.cpp | 669 +++ kcmkwin/kwindesktop/main.h | 92 + kcmkwin/kwindesktop/main.ui | 326 ++ kcmkwin/kwinoptions/AUTHORS | 12 + kcmkwin/kwinoptions/CMakeLists.txt | 18 + kcmkwin/kwinoptions/ChangeLog | 51 + kcmkwin/kwinoptions/Messages.sh | 3 + kcmkwin/kwinoptions/actions.ui | 660 +++ kcmkwin/kwinoptions/advanced.ui | 283 ++ kcmkwin/kwinoptions/focus.ui | 585 +++ kcmkwin/kwinoptions/kwinactions.desktop | 188 + kcmkwin/kwinoptions/kwinadvanced.desktop | 186 + kcmkwin/kwinoptions/kwinfocus.desktop | 182 + kcmkwin/kwinoptions/kwinmoving.desktop | 185 + kcmkwin/kwinoptions/kwinoptions.desktop | 189 + kcmkwin/kwinoptions/main.cpp | 249 ++ kcmkwin/kwinoptions/main.h | 99 + kcmkwin/kwinoptions/mouse.cpp | 570 +++ kcmkwin/kwinoptions/mouse.h | 132 + kcmkwin/kwinoptions/mouse.ui | 836 ++++ kcmkwin/kwinoptions/moving.ui | 270 ++ kcmkwin/kwinoptions/windows.cpp | 654 +++ kcmkwin/kwinoptions/windows.h | 191 + kcmkwin/kwinrules/CMakeLists.txt | 62 + kcmkwin/kwinrules/Messages.sh | 4 + kcmkwin/kwinrules/detectwidget.cpp | 176 + kcmkwin/kwinrules/detectwidget.h | 83 + kcmkwin/kwinrules/detectwidget.ui | 229 + kcmkwin/kwinrules/editshortcut.ui | 161 + kcmkwin/kwinrules/kcm.cpp | 106 + kcmkwin/kwinrules/kcm.h | 52 + kcmkwin/kwinrules/kwinrules.desktop | 162 + kcmkwin/kwinrules/kwinsrc.cpp | 55 + kcmkwin/kwinrules/main.cpp | 275 ++ kcmkwin/kwinrules/ruleslist.cpp | 264 ++ kcmkwin/kwinrules/ruleslist.h | 57 + kcmkwin/kwinrules/ruleslist.ui | 122 + kcmkwin/kwinrules/ruleswidget.cpp | 1011 +++++ kcmkwin/kwinrules/ruleswidget.h | 180 + kcmkwin/kwinrules/ruleswidgetbase.ui | 2834 +++++++++++++ kcmkwin/kwinrules/yesnobox.h | 57 + kcmkwin/kwinscreenedges/CMakeLists.txt | 36 + kcmkwin/kwinscreenedges/Messages.sh | 4 + .../kwinscreenedges/kwinscreenedges.desktop | 165 + .../kwinscreenedges/kwintouchscreen.desktop | 120 + kcmkwin/kwinscreenedges/main.cpp | 535 +++ kcmkwin/kwinscreenedges/main.h | 98 + kcmkwin/kwinscreenedges/main.ui | 356 ++ kcmkwin/kwinscreenedges/monitor.cpp | 292 ++ kcmkwin/kwinscreenedges/monitor.h | 113 + .../kwinscreenedges/screenpreviewwidget.cpp | 163 + kcmkwin/kwinscreenedges/screenpreviewwidget.h | 58 + kcmkwin/kwinscreenedges/touch.cpp | 472 +++ kcmkwin/kwinscreenedges/touch.h | 95 + kcmkwin/kwinscreenedges/touch.ui | 71 + kcmkwin/kwinscripts/CMakeLists.txt | 27 + kcmkwin/kwinscripts/Messages.sh | 4 + kcmkwin/kwinscripts/kwinscripts.desktop | 163 + kcmkwin/kwinscripts/kwinscripts.knsrc | 45 + kcmkwin/kwinscripts/main.cpp | 26 + kcmkwin/kwinscripts/module.cpp | 165 + kcmkwin/kwinscripts/module.h | 73 + kcmkwin/kwinscripts/module.ui | 95 + kcmkwin/kwinscripts/version.h.cmake | 7 + kcmkwin/kwintabbox/CMakeLists.txt | 40 + kcmkwin/kwintabbox/Messages.sh | 4 + kcmkwin/kwintabbox/kwinswitcher.knsrc | 46 + kcmkwin/kwintabbox/kwintabbox.desktop | 156 + kcmkwin/kwintabbox/layoutpreview.cpp | 260 ++ kcmkwin/kwintabbox/layoutpreview.h | 144 + kcmkwin/kwintabbox/main.cpp | 592 +++ kcmkwin/kwintabbox/main.h | 96 + kcmkwin/kwintabbox/main.ui | 720 ++++ kcmkwin/kwintabbox/thumbnailitem.cpp | 219 + kcmkwin/kwintabbox/thumbnailitem.h | 108 + kcmkwin/kwintabbox/thumbnails/dolphin.png | Bin 0 -> 53721 bytes kcmkwin/kwintabbox/thumbnails/kmail.png | Bin 0 -> 64124 bytes kcmkwin/kwintabbox/thumbnails/konqueror.png | Bin 0 -> 76360 bytes .../kwintabbox/thumbnails/systemsettings.png | Bin 0 -> 62546 bytes kconf_update/CMakeLists.txt | 2 + kconf_update/kwin.upd | 7 + keyboard_input.cpp | 267 ++ keyboard_input.h | 104 + keyboard_layout.cpp | 342 ++ keyboard_layout.h | 113 + keyboard_layout_switching.cpp | 258 ++ keyboard_layout_switching.h | 138 + keyboard_repeat.cpp | 73 + keyboard_repeat.h | 56 + killwindow.cpp | 61 + killwindow.h | 41 + kwin.kcfg | 307 ++ kwin.notifyrc | 285 ++ kwinbindings.cpp | 169 + lanczosfilter.cpp | 426 ++ lanczosfilter.h | 77 + layers.cpp | 822 ++++ libinput/connection.cpp | 685 +++ libinput/connection.h | 172 + libinput/context.cpp | 184 + libinput/context.h | 80 + libinput/device.cpp | 500 +++ libinput/device.h | 537 +++ libinput/events.cpp | 330 ++ libinput/events.h | 201 + libinput/libinput_logging.cpp | 21 + libinput/libinput_logging.h | 26 + libkwineffects/CMakeLists.txt | 120 + libkwineffects/Mainpage.dox | 22 + libkwineffects/Messages.sh | 2 + libkwineffects/anidata.cpp | 201 + libkwineffects/anidata_p.h | 60 + libkwineffects/kwinanimationeffect.cpp | 958 +++++ libkwineffects/kwinanimationeffect.h | 239 ++ libkwineffects/kwinconfig.h.cmake | 26 + libkwineffects/kwineffects.cpp | 2072 +++++++++ libkwineffects/kwineffects.h | 3722 +++++++++++++++++ libkwineffects/kwinglobals.h | 256 ++ libkwineffects/kwinglplatform.cpp | 1162 +++++ libkwineffects/kwinglplatform.h | 433 ++ libkwineffects/kwingltexture.cpp | 658 +++ libkwineffects/kwingltexture.h | 153 + libkwineffects/kwingltexture_p.h | 91 + libkwineffects/kwinglutils.cpp | 2319 ++++++++++ libkwineffects/kwinglutils.h | 825 ++++ libkwineffects/kwinglutils_funcs.cpp | 102 + libkwineffects/kwinglutils_funcs.h | 60 + libkwineffects/kwinxrenderutils.cpp | 296 ++ libkwineffects/kwinxrenderutils.h | 194 + libkwineffects/logging.cpp | 23 + libkwineffects/logging_p.h | 30 + logind.cpp | 461 ++ logind.h | 112 + main.cpp | 458 ++ main.h | 264 ++ main_wayland.cpp | 808 ++++ main_wayland.h | 81 + main_x11.cpp | 469 +++ main_x11.h | 70 + manage.cpp | 792 ++++ modifier_only_shortcuts.cpp | 102 + modifier_only_shortcuts.h | 56 + moving_client_x11_filter.cpp | 62 + moving_client_x11_filter.h | 38 + netinfo.cpp | 304 ++ netinfo.h | 100 + onscreennotification.cpp | 244 ++ onscreennotification.h | 94 + options.cpp | 1148 +++++ options.h | 943 +++++ org.freedesktop.ScreenSaver.xml | 41 + org.kde.KWin.xml | 44 + org.kde.kappmenu.xml | 28 + org.kde.kwin.ColorCorrect.xml | 22 + org.kde.kwin.Compositing.xml | 19 + org.kde.kwin.Effects.xml | 43 + org.kde.kwin.OrientationSensor.xml | 6 + orientation_sensor.cpp | 153 + orientation_sensor.h | 91 + osd.cpp | 82 + osd.h | 45 + outline.cpp | 186 + outline.h | 194 + outputscreens.cpp | 132 + outputscreens.h | 55 + overlaywindow.cpp | 32 + overlaywindow.h | 52 + packageplugins/CMakeLists.txt | 4 + packageplugins/aurorae/CMakeLists.txt | 17 + packageplugins/aurorae/aurorae.cpp | 85 + packageplugins/aurorae/aurorae.h | 33 + .../kwin-packagestructure-aurorae.desktop | 44 + packageplugins/decoration/CMakeLists.txt | 17 + packageplugins/decoration/decoration.cpp | 62 + packageplugins/decoration/decoration.h | 33 + .../kwin-packagestructure-decoration.desktop | 47 + packageplugins/scripts/CMakeLists.txt | 16 + .../kwin-packagestructure-scripts.desktop | 47 + packageplugins/scripts/scripts.cpp | 62 + packageplugins/scripts/scripts.h | 33 + packageplugins/windowswitcher/CMakeLists.txt | 16 + ...in-packagestructure-windowswitcher.desktop | 47 + .../windowswitcher/windowswitcher.cpp | 62 + .../windowswitcher/windowswitcher.h | 33 + placement.cpp | 965 +++++ placement.h | 112 + platform.cpp | 492 +++ platform.h | 540 +++ platformsupport/CMakeLists.txt | 1 + platformsupport/scenes/CMakeLists.txt | 2 + platformsupport/scenes/opengl/CMakeLists.txt | 23 + .../scenes/opengl/abstract_egl_backend.cpp | 544 +++ .../scenes/opengl/abstract_egl_backend.h | 122 + platformsupport/scenes/opengl/backend.cpp | 119 + platformsupport/scenes/opengl/backend.h | 325 ++ .../scenes/opengl/swap_profiler.cpp | 57 + platformsupport/scenes/opengl/swap_profiler.h | 53 + platformsupport/scenes/opengl/texture.cpp | 80 + platformsupport/scenes/opengl/texture.h | 76 + .../scenes/qpainter/CMakeLists.txt | 16 + platformsupport/scenes/qpainter/backend.cpp | 68 + platformsupport/scenes/qpainter/backend.h | 108 + plugins/CMakeLists.txt | 9 + plugins/idletime/CMakeLists.txt | 17 + plugins/idletime/kwin.json | 3 + plugins/idletime/poller.cpp | 134 + plugins/idletime/poller.h | 67 + plugins/kdecorations/CMakeLists.txt | 1 + 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 | 65 + plugins/kdecorations/aurorae/src/aurorae.cpp | 788 ++++ plugins/kdecorations/aurorae/src/aurorae.h | 135 + plugins/kdecorations/aurorae/src/aurorae.json | 22 + .../kdecorations/aurorae/src/aurorae.knsrc | 42 + .../kdecorations/aurorae/src/colorhelper.cpp | 63 + .../kdecorations/aurorae/src/colorhelper.h | 240 ++ .../aurorae/src/decorationoptions.cpp | 269 ++ .../aurorae/src/decorationoptions.h | 318 ++ .../aurorae/src/decorationplugin.cpp | 29 + .../aurorae/src/decorationplugin.h | 29 + .../aurorae/src/kwindecoration.desktop | 61 + .../aurorae/src/lib/auroraetheme.cpp | 505 +++ .../aurorae/src/lib/auroraetheme.h | 229 + .../aurorae/src/lib/themeconfig.cpp | 201 + .../aurorae/src/lib/themeconfig.h | 413 ++ .../aurorae/src/qml/AppMenuButton.qml | 29 + .../aurorae/src/qml/AuroraeButton.qml | 215 + .../aurorae/src/qml/AuroraeButtonGroup.qml | 60 + .../aurorae/src/qml/AuroraeMaximizeButton.qml | 69 + .../aurorae/src/qml/ButtonGroup.qml | 101 + .../aurorae/src/qml/Decoration.qml | 36 + .../aurorae/src/qml/DecorationButton.qml | 125 + .../aurorae/src/qml/MenuButton.qml | 81 + .../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 | 8 + .../themes/plastik/code/CMakeLists.txt | 9 + .../themes/plastik/code/plastikbutton.cpp | 470 +++ .../themes/plastik/code/plastikbutton.h | 84 + .../themes/plastik/code/plastikplugin.cpp | 33 + .../themes/plastik/code/plastikplugin.h | 31 + .../aurorae/themes/plastik/code/qmldir | 5 + .../plastik/package/contents/config/main.xml | 28 + .../package/contents/ui/PlastikButton.qml | 160 + .../plastik/package/contents/ui/config.ui | 99 + .../plastik/package/contents/ui/main.qml | 429 ++ .../themes/plastik/package/metadata.desktop | 150 + plugins/kglobalaccel/CMakeLists.txt | 17 + plugins/kglobalaccel/kglobalaccel_plugin.cpp | 58 + plugins/kglobalaccel/kglobalaccel_plugin.h | 48 + plugins/kglobalaccel/kwin.json | 3 + plugins/platforms/CMakeLists.txt | 12 + plugins/platforms/drm/CMakeLists.txt | 38 + plugins/platforms/drm/drm.json | 77 + plugins/platforms/drm/drm_backend.cpp | 793 ++++ plugins/platforms/drm/drm_backend.h | 197 + plugins/platforms/drm/drm_buffer.cpp | 107 + plugins/platforms/drm/drm_buffer.h | 88 + plugins/platforms/drm/drm_buffer_gbm.cpp | 68 + plugins/platforms/drm/drm_buffer_gbm.h | 67 + .../platforms/drm/drm_inputeventfilter.cpp | 115 + plugins/platforms/drm/drm_inputeventfilter.h | 56 + plugins/platforms/drm/drm_object.cpp | 155 + plugins/platforms/drm/drm_object.h | 136 + .../platforms/drm/drm_object_connector.cpp | 80 + plugins/platforms/drm/drm_object_connector.h | 57 + plugins/platforms/drm/drm_object_crtc.cpp | 121 + plugins/platforms/drm/drm_object_crtc.h | 88 + plugins/platforms/drm/drm_object_plane.cpp | 200 + plugins/platforms/drm/drm_object_plane.h | 122 + plugins/platforms/drm/drm_output.cpp | 1295 ++++++ plugins/platforms/drm/drm_output.h | 184 + plugins/platforms/drm/drm_pointer.h | 41 + plugins/platforms/drm/egl_gbm_backend.cpp | 426 ++ plugins/platforms/drm/egl_gbm_backend.h | 101 + plugins/platforms/drm/gbm_surface.cpp | 55 + plugins/platforms/drm/gbm_surface.h | 55 + plugins/platforms/drm/logging.cpp | 21 + plugins/platforms/drm/logging.h | 26 + .../platforms/drm/remoteaccess_manager.cpp | 88 + plugins/platforms/drm/remoteaccess_manager.h | 61 + .../drm/scene_qpainter_drm_backend.cpp | 144 + .../drm/scene_qpainter_drm_backend.h | 60 + plugins/platforms/drm/screens_drm.cpp | 55 + plugins/platforms/drm/screens_drm.h | 43 + plugins/platforms/fbdev/CMakeLists.txt | 15 + plugins/platforms/fbdev/fb_backend.cpp | 260 ++ plugins/platforms/fbdev/fb_backend.h | 111 + plugins/platforms/fbdev/fbdev.json | 77 + plugins/platforms/fbdev/logging.cpp | 21 + plugins/platforms/fbdev/logging.h | 26 + .../fbdev/scene_qpainter_fb_backend.cpp | 91 + .../fbdev/scene_qpainter_fb_backend.h | 52 + plugins/platforms/hwcomposer/CMakeLists.txt | 23 + .../hwcomposer/egl_hwcomposer_backend.cpp | 184 + .../hwcomposer/egl_hwcomposer_backend.h | 66 + plugins/platforms/hwcomposer/hwcomposer.json | 77 + .../hwcomposer/hwcomposer_backend.cpp | 485 +++ .../platforms/hwcomposer/hwcomposer_backend.h | 160 + plugins/platforms/hwcomposer/logging.cpp | 21 + plugins/platforms/hwcomposer/logging.h | 26 + .../hwcomposer/screens_hwcomposer.cpp | 49 + .../platforms/hwcomposer/screens_hwcomposer.h | 43 + plugins/platforms/virtual/CMakeLists.txt | 25 + plugins/platforms/virtual/egl_gbm_backend.cpp | 293 ++ plugins/platforms/virtual/egl_gbm_backend.h | 75 + .../scene_qpainter_virtual_backend.cpp | 89 + .../virtual/scene_qpainter_virtual_backend.h | 58 + plugins/platforms/virtual/screens_virtual.cpp | 53 + plugins/platforms/virtual/screens_virtual.h | 45 + plugins/platforms/virtual/virtual.json | 78 + plugins/platforms/virtual/virtual_backend.cpp | 151 + plugins/platforms/virtual/virtual_backend.h | 94 + plugins/platforms/virtual/virtual_output.cpp | 49 + plugins/platforms/virtual/virtual_output.h | 64 + plugins/platforms/wayland/CMakeLists.txt | 24 + .../platforms/wayland/egl_wayland_backend.cpp | 290 ++ .../platforms/wayland/egl_wayland_backend.h | 95 + plugins/platforms/wayland/logging.cpp | 21 + plugins/platforms/wayland/logging.h | 26 + .../scene_qpainter_wayland_backend.cpp | 134 + .../wayland/scene_qpainter_wayland_backend.h | 68 + plugins/platforms/wayland/wayland.json | 77 + plugins/platforms/wayland/wayland_backend.cpp | 668 +++ plugins/platforms/wayland/wayland_backend.h | 208 + plugins/platforms/x11/CMakeLists.txt | 5 + plugins/platforms/x11/common/CMakeLists.txt | 9 + .../platforms/x11/common/eglonxbackend.cpp | 544 +++ plugins/platforms/x11/common/eglonxbackend.h | 108 + .../platforms/x11/standalone/CMakeLists.txt | 43 + plugins/platforms/x11/standalone/edge.cpp | 144 + plugins/platforms/x11/standalone/edge.h | 79 + .../effects_mouse_interception_x11_filter.cpp | 65 + .../effects_mouse_interception_x11_filter.h | 43 + .../platforms/x11/standalone/effects_x11.cpp | 114 + .../platforms/x11/standalone/effects_x11.h | 58 + .../glx_context_attribute_builder.cpp | 53 + .../glx_context_attribute_builder.h | 32 + .../platforms/x11/standalone/glxbackend.cpp | 936 +++++ plugins/platforms/x11/standalone/glxbackend.h | 150 + plugins/platforms/x11/standalone/logging.cpp | 21 + plugins/platforms/x11/standalone/logging.h | 26 + .../x11/standalone/non_composited_outline.cpp | 150 + .../x11/standalone/non_composited_outline.h | 59 + .../x11/standalone/overlaywindow_x11.cpp | 213 + .../x11/standalone/overlaywindow_x11.h | 57 + .../x11/standalone/screenedges_filter.cpp | 65 + .../x11/standalone/screenedges_filter.h | 37 + .../x11/standalone/screens_xrandr.cpp | 230 + .../platforms/x11/standalone/screens_xrandr.h | 61 + .../platforms/x11/standalone/sync_filter.cpp | 48 + .../platforms/x11/standalone/sync_filter.h | 38 + .../x11/standalone/windowselector.cpp | 273 ++ .../platforms/x11/standalone/windowselector.h | 70 + plugins/platforms/x11/standalone/x11.json | 76 + .../standalone/x11_decoration_renderer.cpp | 109 + .../x11/standalone/x11_decoration_renderer.h | 55 + .../platforms/x11/standalone/x11_platform.cpp | 442 ++ .../platforms/x11/standalone/x11_platform.h | 102 + .../platforms/x11/standalone/x11cursor.cpp | 196 + plugins/platforms/x11/standalone/x11cursor.h | 85 + .../standalone/xfixes_cursor_event_filter.cpp | 40 + .../standalone/xfixes_cursor_event_filter.h | 41 + .../x11/standalone/xinputintegration.cpp | 312 ++ .../x11/standalone/xinputintegration.h | 69 + plugins/platforms/x11/windowed/CMakeLists.txt | 17 + .../x11/windowed/egl_x11_backend.cpp | 121 + .../platforms/x11/windowed/egl_x11_backend.h | 57 + plugins/platforms/x11/windowed/logging.cpp | 21 + plugins/platforms/x11/windowed/logging.h | 26 + .../windowed/scene_qpainter_x11_backend.cpp | 104 + .../x11/windowed/scene_qpainter_x11_backend.h | 65 + plugins/platforms/x11/windowed/x11.json | 78 + .../x11/windowed/x11windowed_backend.cpp | 495 +++ .../x11/windowed/x11windowed_backend.h | 113 + plugins/qpa/CMakeLists.txt | 50 + plugins/qpa/abstractplatformcontext.cpp | 257 ++ plugins/qpa/abstractplatformcontext.h | 68 + plugins/qpa/backingstore.cpp | 119 + plugins/qpa/backingstore.h | 60 + plugins/qpa/integration.cpp | 294 ++ plugins/qpa/integration.h | 88 + plugins/qpa/kwin.json | 3 + plugins/qpa/main.cpp | 48 + plugins/qpa/nativeinterface.cpp | 106 + plugins/qpa/nativeinterface.h | 47 + plugins/qpa/platformcontextwayland.cpp | 77 + plugins/qpa/platformcontextwayland.h | 49 + plugins/qpa/platformcursor.cpp | 53 + plugins/qpa/platformcursor.h | 43 + plugins/qpa/screen.cpp | 80 + plugins/qpa/screen.h | 54 + plugins/qpa/sharingplatformcontext.cpp | 114 + plugins/qpa/sharingplatformcontext.h | 54 + plugins/qpa/window.cpp | 177 + plugins/qpa/window.h | 104 + plugins/scenes/CMakeLists.txt | 5 + plugins/scenes/opengl/CMakeLists.txt | 27 + plugins/scenes/opengl/opengl.json | 71 + plugins/scenes/opengl/scene_opengl.cpp | 2610 ++++++++++++ plugins/scenes/opengl/scene_opengl.h | 357 ++ plugins/scenes/qpainter/CMakeLists.txt | 15 + plugins/scenes/qpainter/qpainter.json | 73 + plugins/scenes/qpainter/scene_qpainter.cpp | 876 ++++ plugins/scenes/qpainter/scene_qpainter.h | 202 + plugins/scenes/xrender/CMakeLists.txt | 27 + plugins/scenes/xrender/scene_xrender.cpp | 1326 ++++++ plugins/scenes/xrender/scene_xrender.h | 361 ++ plugins/scenes/xrender/xrender.json | 73 + po/af/kcmkwindecoration.po | 206 + po/af/kcmkwinrules.po | 1349 ++++++ po/af/kcmkwm.po | 1481 +++++++ po/af/kwin.po | 2070 +++++++++ po/af/kwin_clients.po | 148 + po/ar/kcm-kwin-scripts.po | 86 + po/ar/kcm_kwindesktop.po | 256 ++ po/ar/kcm_kwintabbox.po | 218 + po/ar/kcmkwincompositing.po | 352 ++ po/ar/kcmkwindecoration.po | 204 + po/ar/kcmkwinrules.po | 1270 ++++++ po/ar/kcmkwinscreenedges.po | 241 ++ po/ar/kcmkwm.po | 1364 ++++++ po/ar/kwin.po | 2075 +++++++++ po/ar/kwin_clients.po | 145 + po/ar/kwin_effects.po | 2186 ++++++++++ po/ar/kwin_scripting.po | 114 + po/as/kwin.po | 2066 +++++++++ po/ast/kcm-kwin-scripts.po | 85 + po/ast/kcm_kwindesktop.po | 250 ++ po/ast/kcm_kwintabbox.po | 218 + po/ast/kcmkwincompositing.po | 330 ++ po/ast/kcmkwindecoration.po | 194 + po/ast/kcmkwinrules.po | 1253 ++++++ po/ast/kcmkwinscreenedges.po | 230 + po/ast/kcmkwm.po | 1295 ++++++ po/ast/kwin.po | 2003 +++++++++ po/ast/kwin_clients.po | 137 + po/ast/kwin_effects.po | 2152 ++++++++++ po/ast/kwin_scripting.po | 109 + po/ast/kwin_scripts.po | 61 + po/be/kcm_kwindesktop.po | 264 ++ po/be/kcmkwincompositing.po | 339 ++ po/be/kcmkwindecoration.po | 210 + po/be/kcmkwinrules.po | 1267 ++++++ po/be/kcmkwm.po | 1333 ++++++ po/be/kwin.po | 2067 +++++++++ po/be/kwin_clients.po | 145 + po/be/kwin_effects.po | 2182 ++++++++++ po/be@latin/kwin.po | 2074 +++++++++ po/bg/kcm_kwindesktop.po | 256 ++ po/bg/kcm_kwintabbox.po | 231 + po/bg/kcmkwincompositing.po | 352 ++ po/bg/kcmkwindecoration.po | 201 + po/bg/kcmkwinrules.po | 1332 ++++++ po/bg/kcmkwinscreenedges.po | 235 ++ po/bg/kcmkwm.po | 1482 +++++++ po/bg/kwin.po | 2071 +++++++++ po/bg/kwin_clients.po | 157 + po/bg/kwin_effects.po | 2192 ++++++++++ po/bn/kcmkwm.po | 1417 +++++++ po/bn/kwin.po | 2054 +++++++++ po/bn/kwin_effects.po | 2203 ++++++++++ po/bn_IN/kcm_kwindesktop.po | 258 ++ po/bn_IN/kcmkwindecoration.po | 207 + po/bn_IN/kcmkwinrules.po | 1267 ++++++ po/bn_IN/kcmkwm.po | 1351 ++++++ po/bn_IN/kwin.po | 2066 +++++++++ po/bn_IN/kwin_clients.po | 143 + po/bn_IN/kwin_effects.po | 2190 ++++++++++ po/br/kcmkwindecoration.po | 204 + po/br/kcmkwinrules.po | 1261 ++++++ po/br/kcmkwm.po | 1331 ++++++ po/br/kwin.po | 2046 +++++++++ po/br/kwin_clients.po | 140 + po/bs/kcm-kwin-scripts.po | 89 + po/bs/kcm_kwindesktop.po | 261 ++ po/bs/kcm_kwintabbox.po | 224 + po/bs/kcmkwincompositing.po | 362 ++ po/bs/kcmkwindecoration.po | 204 + po/bs/kcmkwinrules.po | 1355 ++++++ po/bs/kcmkwinscreenedges.po | 256 ++ po/bs/kcmkwm.po | 1467 +++++++ po/bs/kwin.po | 2076 +++++++++ po/bs/kwin_clients.po | 611 +++ po/bs/kwin_effects.po | 2273 ++++++++++ po/bs/kwin_scripting.po | 116 + po/ca/docs/kcontrol/desktop/index.docbook | 156 + po/ca/docs/kcontrol/kwindecoration/button.png | Bin 0 -> 60426 bytes .../kcontrol/kwindecoration/configure.png | Bin 0 -> 483 bytes .../kcontrol/kwindecoration/decoration.png | Bin 0 -> 911 bytes .../kcontrol/kwindecoration/index.docbook | 169 + po/ca/docs/kcontrol/kwindecoration/main.png | Bin 0 -> 59825 bytes .../kwineffects/configure-effects.png | Bin 0 -> 483 bytes .../kcontrol/kwineffects/configure-filter.png | Bin 0 -> 402 bytes .../kwineffects/dialog-information.png | Bin 0 -> 619 bytes po/ca/docs/kcontrol/kwineffects/index.docbook | 120 + po/ca/docs/kcontrol/kwineffects/video.png | Bin 0 -> 400 bytes .../kcontrol/kwinscreenedges/index.docbook | 80 + po/ca/docs/kcontrol/kwintabbox/index.docbook | 157 + .../kcontrol/windowbehaviour/index.docbook | 821 ++++ .../kcontrol/windowspecific/Face-smile.png | Bin 0 -> 1233 bytes .../kcontrol/windowspecific/index.docbook | 2199 ++++++++++ .../windowspecific/pager-4-desktops.png | Bin 0 -> 11817 bytes po/ca/kcm-kwin-scripts.po | 90 + po/ca/kcm_kwindesktop.po | 264 ++ po/ca/kcm_kwintabbox.po | 229 + po/ca/kcmkwincompositing.po | 357 ++ po/ca/kcmkwindecoration.po | 202 + po/ca/kcmkwinrules.po | 1343 ++++++ po/ca/kcmkwinscreenedges.po | 247 ++ po/ca/kcmkwm.po | 1472 +++++++ po/ca/kwin.po | 2087 +++++++++ po/ca/kwin_clients.po | 150 + po/ca/kwin_effects.po | 2169 ++++++++++ po/ca/kwin_scripting.po | 122 + po/ca/kwin_scripts.po | 64 + po/ca@valencia/kcm-kwin-scripts.po | 90 + po/ca@valencia/kcm_kwindesktop.po | 264 ++ po/ca@valencia/kcm_kwintabbox.po | 229 + po/ca@valencia/kcmkwincompositing.po | 357 ++ po/ca@valencia/kcmkwindecoration.po | 202 + po/ca@valencia/kcmkwinrules.po | 1344 ++++++ po/ca@valencia/kcmkwinscreenedges.po | 247 ++ po/ca@valencia/kcmkwm.po | 1471 +++++++ po/ca@valencia/kwin.po | 2087 +++++++++ po/ca@valencia/kwin_clients.po | 150 + po/ca@valencia/kwin_effects.po | 2169 ++++++++++ po/ca@valencia/kwin_scripting.po | 122 + po/ca@valencia/kwin_scripts.po | 64 + po/cs/kcm-kwin-scripts.po | 88 + po/cs/kcm_kwindesktop.po | 259 ++ po/cs/kcm_kwintabbox.po | 223 + po/cs/kcmkwincompositing.po | 347 ++ po/cs/kcmkwindecoration.po | 200 + po/cs/kcmkwinrules.po | 1326 ++++++ po/cs/kcmkwinscreenedges.po | 237 ++ po/cs/kcmkwm.po | 1412 +++++++ po/cs/kwin.po | 2043 +++++++++ po/cs/kwin_clients.po | 144 + po/cs/kwin_effects.po | 2151 ++++++++++ po/cs/kwin_scripting.po | 113 + po/cs/kwin_scripts.po | 61 + po/csb/kcm_kwindesktop.po | 257 ++ po/csb/kwin.po | 2069 +++++++++ po/csb/kwin_clients.po | 153 + po/csb/kwin_effects.po | 2218 ++++++++++ po/cy/kcmkwindecoration.po | 204 + po/cy/kcmkwinrules.po | 1293 ++++++ po/cy/kcmkwm.po | 1435 +++++++ po/cy/kwin.po | 2069 +++++++++ po/cy/kwin_clients.po | 141 + po/da/kcm-kwin-scripts.po | 87 + po/da/kcm_kwindesktop.po | 260 ++ po/da/kcm_kwintabbox.po | 224 + po/da/kcmkwincompositing.po | 353 ++ po/da/kcmkwindecoration.po | 199 + po/da/kcmkwinrules.po | 1334 ++++++ po/da/kcmkwinscreenedges.po | 239 ++ po/da/kcmkwm.po | 1461 +++++++ po/da/kwin.po | 2083 +++++++++ po/da/kwin_clients.po | 143 + po/da/kwin_effects.po | 2177 ++++++++++ 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 | 134 + .../kcontrol/kwinscreenedges/index.docbook | 94 + po/de/docs/kcontrol/kwintabbox/index.docbook | 169 + .../kcontrol/windowbehaviour/index.docbook | 825 ++++ .../kcontrol/windowspecific/index.docbook | 2215 ++++++++++ po/de/kcm-kwin-scripts.po | 85 + po/de/kcm_kwindesktop.po | 261 ++ po/de/kcm_kwintabbox.po | 226 + po/de/kcmkwincompositing.po | 358 ++ po/de/kcmkwindecoration.po | 198 + po/de/kcmkwinrules.po | 1343 ++++++ po/de/kcmkwinscreenedges.po | 239 ++ po/de/kcmkwm.po | 1486 +++++++ po/de/kwin.po | 2249 ++++++++++ po/de/kwin_clients.po | 147 + po/de/kwin_effects.po | 2188 ++++++++++ po/de/kwin_scripting.po | 115 + po/de/kwin_scripts.po | 58 + po/el/kcm-kwin-scripts.po | 90 + po/el/kcm_kwindesktop.po | 267 ++ po/el/kcm_kwintabbox.po | 226 + po/el/kcmkwincompositing.po | 368 ++ po/el/kcmkwindecoration.po | 207 + po/el/kcmkwinrules.po | 1354 ++++++ po/el/kcmkwinscreenedges.po | 253 ++ po/el/kcmkwm.po | 1493 +++++++ po/el/kwin.po | 2112 ++++++++++ po/el/kwin_clients.po | 151 + po/el/kwin_effects.po | 2204 ++++++++++ po/el/kwin_scripting.po | 119 + po/el/kwin_scripts.po | 61 + po/en_GB/kcm-kwin-scripts.po | 87 + po/en_GB/kcm_kwindesktop.po | 260 ++ po/en_GB/kcm_kwintabbox.po | 221 + po/en_GB/kcmkwincompositing.po | 353 ++ po/en_GB/kcmkwindecoration.po | 198 + po/en_GB/kcmkwinrules.po | 1331 ++++++ po/en_GB/kcmkwinscreenedges.po | 240 ++ po/en_GB/kcmkwm.po | 1450 +++++++ po/en_GB/kwin.po | 2064 +++++++++ po/en_GB/kwin_clients.po | 143 + po/en_GB/kwin_effects.po | 2154 ++++++++++ po/en_GB/kwin_scripting.po | 112 + po/en_GB/kwin_scripts.po | 61 + po/eo/kcm_kwindesktop.po | 258 ++ po/eo/kcm_kwintabbox.po | 231 + po/eo/kcmkwincompositing.po | 344 ++ po/eo/kcmkwindecoration.po | 204 + po/eo/kcmkwinrules.po | 1311 ++++++ po/eo/kcmkwinscreenedges.po | 251 ++ po/eo/kcmkwm.po | 1452 +++++++ po/eo/kwin.po | 2088 +++++++++ po/eo/kwin_clients.po | 147 + po/eo/kwin_effects.po | 2230 ++++++++++ po/es/docs/kcontrol/desktop/index.docbook | 198 + po/es/kcm-kwin-scripts.po | 87 + po/es/kcm_kwindesktop.po | 263 ++ po/es/kcm_kwintabbox.po | 230 + po/es/kcmkwincompositing.po | 361 ++ po/es/kcmkwindecoration.po | 206 + po/es/kcmkwinrules.po | 1344 ++++++ po/es/kcmkwinscreenedges.po | 246 ++ po/es/kcmkwm.po | 1488 +++++++ po/es/kwin.po | 2099 ++++++++++ po/es/kwin_clients.po | 150 + po/es/kwin_effects.po | 2171 ++++++++++ po/es/kwin_scripting.po | 118 + po/es/kwin_scripts.po | 63 + po/et/kcm-kwin-scripts.po | 86 + po/et/kcm_kwindesktop.po | 258 ++ po/et/kcm_kwintabbox.po | 220 + po/et/kcmkwincompositing.po | 354 ++ po/et/kcmkwindecoration.po | 199 + po/et/kcmkwinrules.po | 1329 ++++++ po/et/kcmkwinscreenedges.po | 247 ++ po/et/kcmkwm.po | 1454 +++++++ po/et/kwin.po | 2096 ++++++++++ po/et/kwin_clients.po | 142 + po/et/kwin_effects.po | 2188 ++++++++++ po/et/kwin_scripting.po | 113 + po/et/kwin_scripts.po | 61 + po/eu/kcm-kwin-scripts.po | 90 + po/eu/kcm_kwindesktop.po | 266 ++ po/eu/kcm_kwintabbox.po | 227 + po/eu/kcmkwincompositing.po | 362 ++ po/eu/kcmkwindecoration.po | 203 + po/eu/kcmkwinrules.po | 1352 ++++++ po/eu/kcmkwinscreenedges.po | 278 ++ po/eu/kcmkwm.po | 1466 +++++++ po/eu/kwin.po | 2101 ++++++++++ po/eu/kwin_clients.po | 149 + po/eu/kwin_effects.po | 2171 ++++++++++ po/eu/kwin_scripting.po | 117 + po/eu/kwin_scripts.po | 64 + po/fa/kcm-kwin-scripts.po | 88 + po/fa/kcm_kwindesktop.po | 259 ++ po/fa/kcm_kwintabbox.po | 218 + po/fa/kcmkwincompositing.po | 327 ++ po/fa/kcmkwindecoration.po | 209 + po/fa/kcmkwinrules.po | 1339 ++++++ po/fa/kcmkwinscreenedges.po | 230 + po/fa/kcmkwm.po | 1474 +++++++ po/fa/kwin.po | 2073 +++++++++ po/fa/kwin_clients.po | 150 + po/fa/kwin_effects.po | 2128 ++++++++++ po/fi/kcm-kwin-scripts.po | 89 + po/fi/kcm_kwindesktop.po | 269 ++ po/fi/kcm_kwintabbox.po | 230 + po/fi/kcmkwincompositing.po | 957 +++++ po/fi/kcmkwindecoration.po | 358 ++ po/fi/kcmkwinrules.po | 1342 ++++++ po/fi/kcmkwinscreenedges.po | 245 ++ po/fi/kcmkwm.po | 1473 +++++++ po/fi/kwin.po | 2180 ++++++++++ po/fi/kwin_clients.po | 153 + po/fi/kwin_effects.po | 2172 ++++++++++ po/fi/kwin_scripting.po | 119 + po/fi/kwin_scripts.po | 61 + po/fr/kcm-kwin-scripts.po | 95 + po/fr/kcm_kwindesktop.po | 266 ++ po/fr/kcm_kwintabbox.po | 229 + po/fr/kcmkwincompositing.po | 399 ++ po/fr/kcmkwindecoration.po | 208 + po/fr/kcmkwinrules.po | 1379 ++++++ po/fr/kcmkwinscreenedges.po | 253 ++ po/fr/kcmkwm.po | 1504 +++++++ po/fr/kwin.po | 2111 ++++++++++ po/fr/kwin_clients.po | 155 + po/fr/kwin_effects.po | 2198 ++++++++++ po/fr/kwin_scripting.po | 119 + po/fr/kwin_scripts.po | 64 + po/fy/kcm_kwindesktop.po | 263 ++ po/fy/kcmkwincompositing.po | 352 ++ po/fy/kcmkwindecoration.po | 202 + po/fy/kcmkwinrules.po | 1338 ++++++ po/fy/kcmkwinscreenedges.po | 254 ++ po/fy/kcmkwm.po | 1512 +++++++ po/fy/kwin.po | 2063 +++++++++ po/fy/kwin_clients.po | 158 + po/fy/kwin_effects.po | 2215 ++++++++++ po/ga/kcm-kwin-scripts.po | 87 + po/ga/kcm_kwindesktop.po | 261 ++ po/ga/kcm_kwintabbox.po | 221 + po/ga/kcmkwincompositing.po | 357 ++ po/ga/kcmkwindecoration.po | 204 + po/ga/kcmkwinrules.po | 1322 ++++++ po/ga/kcmkwinscreenedges.po | 255 ++ po/ga/kcmkwm.po | 1693 ++++++++ po/ga/kwin.po | 2057 +++++++++ po/ga/kwin_clients.po | 689 +++ po/ga/kwin_effects.po | 2187 ++++++++++ po/gl/kcm-kwin-scripts.po | 91 + po/gl/kcm_kwindesktop.po | 268 ++ po/gl/kcm_kwintabbox.po | 229 + po/gl/kcmkwincompositing.po | 363 ++ po/gl/kcmkwindecoration.po | 207 + po/gl/kcmkwinrules.po | 1341 ++++++ po/gl/kcmkwinscreenedges.po | 247 ++ po/gl/kcmkwm.po | 1469 +++++++ po/gl/kwin.po | 2090 +++++++++ po/gl/kwin_clients.po | 152 + po/gl/kwin_effects.po | 2167 ++++++++++ po/gl/kwin_scripting.po | 123 + po/gl/kwin_scripts.po | 62 + po/gu/kcm_kwindesktop.po | 263 ++ po/gu/kcm_kwintabbox.po | 227 + po/gu/kcmkwincompositing.po | 349 ++ po/gu/kcmkwindecoration.po | 201 + po/gu/kcmkwinrules.po | 1266 ++++++ po/gu/kcmkwinscreenedges.po | 237 ++ po/gu/kcmkwm.po | 1327 ++++++ po/gu/kwin.po | 2039 +++++++++ po/gu/kwin_clients.po | 146 + po/gu/kwin_effects.po | 2213 ++++++++++ po/he/kcm-kwin-scripts.po | 90 + po/he/kcm_kwindesktop.po | 263 ++ po/he/kcm_kwintabbox.po | 225 + po/he/kcmkwincompositing.po | 339 ++ po/he/kcmkwindecoration.po | 207 + po/he/kcmkwinrules.po | 1271 ++++++ po/he/kcmkwinscreenedges.po | 242 ++ po/he/kcmkwm.po | 1437 +++++++ po/he/kwin.po | 2049 +++++++++ po/he/kwin_clients.po | 142 + po/he/kwin_effects.po | 2154 ++++++++++ po/he/kwin_scripting.po | 106 + po/he/kwin_scripts.po | 58 + po/hi/kcm_kwindesktop.po | 263 ++ po/hi/kcm_kwintabbox.po | 230 + po/hi/kcmkwincompositing.po | 343 ++ po/hi/kcmkwindecoration.po | 209 + po/hi/kcmkwinrules.po | 1272 ++++++ po/hi/kcmkwinscreenedges.po | 237 ++ po/hi/kcmkwm.po | 1390 ++++++ po/hi/kwin.po | 2078 +++++++++ po/hi/kwin_clients.po | 155 + po/hi/kwin_effects.po | 2232 ++++++++++ po/hne/kcm_kwindesktop.po | 264 ++ po/hne/kcmkwincompositing.po | 353 ++ po/hne/kcmkwindecoration.po | 210 + po/hne/kcmkwinrules.po | 1272 ++++++ po/hne/kcmkwm.po | 1384 ++++++ po/hne/kwin.po | 2072 +++++++++ po/hne/kwin_clients.po | 149 + po/hne/kwin_effects.po | 2205 ++++++++++ po/hr/kcm_kwindesktop.po | 266 ++ po/hr/kcm_kwintabbox.po | 241 ++ po/hr/kcmkwincompositing.po | 362 ++ po/hr/kcmkwindecoration.po | 207 + po/hr/kcmkwinrules.po | 1344 ++++++ po/hr/kcmkwinscreenedges.po | 257 ++ po/hr/kcmkwm.po | 1487 +++++++ po/hr/kwin.po | 2067 +++++++++ po/hr/kwin_clients.po | 157 + po/hr/kwin_effects.po | 2215 ++++++++++ po/hsb/kcm_kwindesktop.po | 254 ++ po/hsb/kcmkwincompositing.po | 353 ++ po/hsb/kcmkwindecoration.po | 209 + po/hsb/kcmkwinrules.po | 1260 ++++++ po/hsb/kcmkwm.po | 1371 ++++++ po/hsb/kwin.po | 2069 +++++++++ po/hsb/kwin_clients.po | 139 + po/hsb/kwin_effects.po | 2187 ++++++++++ po/hu/kcm-kwin-scripts.po | 89 + po/hu/kcm_kwindesktop.po | 262 ++ po/hu/kcm_kwintabbox.po | 225 + po/hu/kcmkwincompositing.po | 353 ++ po/hu/kcmkwindecoration.po | 199 + po/hu/kcmkwinrules.po | 1330 ++++++ po/hu/kcmkwinscreenedges.po | 244 ++ po/hu/kcmkwm.po | 1461 +++++++ po/hu/kwin.po | 2104 ++++++++++ po/hu/kwin_clients.po | 144 + po/hu/kwin_effects.po | 2192 ++++++++++ po/hu/kwin_scripting.po | 115 + po/hu/kwin_scripts.po | 61 + po/ia/kcm-kwin-scripts.po | 86 + po/ia/kcm_kwindesktop.po | 259 ++ po/ia/kcm_kwintabbox.po | 224 + po/ia/kcmkwincompositing.po | 338 ++ po/ia/kcmkwindecoration.po | 208 + po/ia/kcmkwinrules.po | 1339 ++++++ po/ia/kcmkwinscreenedges.po | 249 ++ po/ia/kcmkwm.po | 1482 +++++++ po/ia/kwin.po | 2087 +++++++++ po/ia/kwin_clients.po | 143 + po/ia/kwin_effects.po | 2202 ++++++++++ po/ia/kwin_scripting.po | 117 + po/ia/kwin_scripts.po | 61 + po/id/kcm-kwin-scripts.po | 87 + po/id/kcm_kwindesktop.po | 262 ++ po/id/kcm_kwintabbox.po | 224 + po/id/kcmkwincompositing.po | 357 ++ po/id/kcmkwindecoration.po | 202 + po/id/kcmkwinrules.po | 1337 ++++++ po/id/kcmkwinscreenedges.po | 239 ++ po/id/kcmkwm.po | 1459 +++++++ po/id/kwin.po | 2070 +++++++++ po/id/kwin_clients.po | 149 + po/id/kwin_effects.po | 2163 ++++++++++ po/id/kwin_scripting.po | 111 + po/id/kwin_scripts.po | 61 + po/is/kcm_kwindesktop.po | 260 ++ po/is/kcm_kwintabbox.po | 239 ++ po/is/kcmkwincompositing.po | 355 ++ po/is/kcmkwindecoration.po | 208 + po/is/kcmkwinrules.po | 1338 ++++++ po/is/kcmkwinscreenedges.po | 255 ++ po/is/kcmkwm.po | 1484 +++++++ po/is/kwin.po | 2069 +++++++++ po/is/kwin_clients.po | 153 + po/is/kwin_effects.po | 2218 ++++++++++ po/it/docs/kcontrol/desktop/index.docbook | 178 + .../kcontrol/kwindecoration/index.docbook | 201 + po/it/docs/kcontrol/kwineffects/index.docbook | 134 + .../kcontrol/kwinscreenedges/index.docbook | 94 + po/it/docs/kcontrol/kwintabbox/index.docbook | 173 + .../kcontrol/windowbehaviour/index.docbook | 841 ++++ .../kcontrol/windowspecific/index.docbook | 2205 ++++++++++ po/it/kcm-kwin-scripts.po | 88 + po/it/kcm_kwindesktop.po | 263 ++ po/it/kcm_kwintabbox.po | 226 + po/it/kcmkwincompositing.po | 360 ++ po/it/kcmkwindecoration.po | 281 ++ po/it/kcmkwinrules.po | 1346 ++++++ po/it/kcmkwinscreenedges.po | 242 ++ po/it/kcmkwm.po | 1475 +++++++ po/it/kwin.po | 2094 ++++++++++ po/it/kwin_clients.po | 146 + po/it/kwin_effects.po | 2172 ++++++++++ po/it/kwin_scripting.po | 116 + po/it/kwin_scripts.po | 62 + po/ja/kcm-kwin-scripts.po | 82 + po/ja/kcm_kwindesktop.po | 261 ++ po/ja/kcm_kwintabbox.po | 226 + po/ja/kcmkwincompositing.po | 339 ++ po/ja/kcmkwindecoration.po | 201 + po/ja/kcmkwinrules.po | 1314 ++++++ po/ja/kcmkwinscreenedges.po | 248 ++ po/ja/kcmkwm.po | 1495 +++++++ po/ja/kwin.po | 2067 +++++++++ po/ja/kwin_clients.po | 148 + po/ja/kwin_effects.po | 2205 ++++++++++ po/ja/kwin_scripting.po | 106 + po/ja/kwin_scripts.po | 58 + po/kk/kcm-kwin-scripts.po | 86 + po/kk/kcm_kwindesktop.po | 262 ++ po/kk/kcm_kwintabbox.po | 220 + po/kk/kcmkwincompositing.po | 367 ++ po/kk/kcmkwindecoration.po | 208 + po/kk/kcmkwinrules.po | 1329 ++++++ po/kk/kcmkwinscreenedges.po | 250 ++ po/kk/kcmkwm.po | 1473 +++++++ po/kk/kwin.po | 2096 ++++++++++ po/kk/kwin_clients.po | 157 + po/kk/kwin_effects.po | 2191 ++++++++++ po/kk/kwin_scripting.po | 115 + po/km/kcm-kwin-scripts.po | 90 + po/km/kcm_kwindesktop.po | 258 ++ po/km/kcm_kwintabbox.po | 221 + po/km/kcmkwincompositing.po | 359 ++ po/km/kcmkwindecoration.po | 201 + po/km/kcmkwinrules.po | 1442 +++++++ po/km/kcmkwinscreenedges.po | 246 ++ po/km/kcmkwm.po | 1436 +++++++ po/km/kwin.po | 2049 +++++++++ po/km/kwin_clients.po | 599 +++ po/km/kwin_effects.po | 2196 ++++++++++ po/kn/kcm_kwindesktop.po | 262 ++ po/kn/kcm_kwintabbox.po | 237 ++ po/kn/kcmkwincompositing.po | 353 ++ po/kn/kcmkwindecoration.po | 211 + po/kn/kcmkwinrules.po | 1341 ++++++ po/kn/kcmkwinscreenedges.po | 245 ++ po/kn/kcmkwm.po | 1481 +++++++ po/kn/kwin.po | 2063 +++++++++ po/kn/kwin_clients.po | 143 + po/kn/kwin_effects.po | 2217 ++++++++++ po/ko/kcm-kwin-scripts.po | 87 + po/ko/kcm_kwindesktop.po | 257 ++ po/ko/kcm_kwintabbox.po | 219 + po/ko/kcmkwincompositing.po | 351 ++ po/ko/kcmkwindecoration.po | 198 + po/ko/kcmkwinrules.po | 1320 ++++++ po/ko/kcmkwinscreenedges.po | 230 + po/ko/kcmkwm.po | 1425 +++++++ po/ko/kwin.po | 2073 +++++++++ po/ko/kwin_clients.po | 146 + po/ko/kwin_effects.po | 2154 ++++++++++ po/ko/kwin_scripting.po | 110 + po/ko/kwin_scripts.po | 61 + po/ku/kcm_kwindesktop.po | 264 ++ po/ku/kcmkwincompositing.po | 349 ++ po/ku/kcmkwindecoration.po | 210 + po/ku/kcmkwinrules.po | 1271 ++++++ po/ku/kcmkwm.po | 1386 ++++++ po/ku/kwin.po | 2052 +++++++++ po/ku/kwin_clients.po | 151 + po/ku/kwin_effects.po | 2180 ++++++++++ po/lt/kcm-kwin-scripts.po | 89 + po/lt/kcm_kwindesktop.po | 261 ++ po/lt/kcm_kwintabbox.po | 227 + po/lt/kcmkwincompositing.po | 339 ++ po/lt/kcmkwindecoration.po | 199 + po/lt/kcmkwinrules.po | 1340 ++++++ po/lt/kcmkwinscreenedges.po | 252 ++ po/lt/kcmkwm.po | 1446 +++++++ po/lt/kwin.po | 2050 +++++++++ po/lt/kwin_clients.po | 146 + po/lt/kwin_effects.po | 2201 ++++++++++ po/lt/kwin_scripting.po | 112 + po/lv/kcm_kwindesktop.po | 264 ++ po/lv/kcm_kwintabbox.po | 238 ++ po/lv/kcmkwincompositing.po | 364 ++ po/lv/kcmkwindecoration.po | 205 + po/lv/kcmkwinrules.po | 1360 ++++++ po/lv/kcmkwinscreenedges.po | 248 ++ po/lv/kcmkwm.po | 1479 +++++++ po/lv/kwin.po | 2074 +++++++++ po/lv/kwin_clients.po | 147 + po/lv/kwin_effects.po | 2179 ++++++++++ po/mai/kcm_kwindesktop.po | 257 ++ po/mai/kcm_kwintabbox.po | 225 + po/mai/kcmkwincompositing.po | 355 ++ po/mai/kcmkwindecoration.po | 214 + po/mai/kcmkwinrules.po | 1275 ++++++ po/mai/kcmkwinscreenedges.po | 239 ++ po/mai/kcmkwm.po | 1392 ++++++ po/mai/kwin.po | 2070 +++++++++ po/mai/kwin_clients.po | 158 + po/mai/kwin_effects.po | 2210 ++++++++++ po/mk/kcm_kwindesktop.po | 260 ++ po/mk/kcmkwincompositing.po | 353 ++ po/mk/kcmkwindecoration.po | 211 + po/mk/kcmkwinrules.po | 1345 ++++++ po/mk/kcmkwm.po | 1483 +++++++ po/mk/kwin.po | 2068 +++++++++ po/mk/kwin_clients.po | 152 + po/mk/kwin_effects.po | 2208 ++++++++++ po/ml/kcm_kwindesktop.po | 264 ++ po/ml/kcmkwincompositing.po | 339 ++ po/ml/kcmkwindecoration.po | 202 + po/ml/kcmkwinrules.po | 1267 ++++++ po/ml/kcmkwinscreenedges.po | 250 ++ po/ml/kcmkwm.po | 1377 ++++++ po/ml/kwin.po | 2043 +++++++++ po/ml/kwin_clients.po | 152 + po/ml/kwin_effects.po | 2219 ++++++++++ po/mr/kcm-kwin-scripts.po | 91 + po/mr/kcm_kwindesktop.po | 261 ++ po/mr/kcm_kwintabbox.po | 220 + po/mr/kcmkwincompositing.po | 366 ++ po/mr/kcmkwindecoration.po | 205 + po/mr/kcmkwinrules.po | 1265 ++++++ po/mr/kcmkwinscreenedges.po | 253 ++ po/mr/kcmkwm.po | 1335 ++++++ po/mr/kwin.po | 2045 +++++++++ po/mr/kwin_clients.po | 146 + po/mr/kwin_effects.po | 2186 ++++++++++ po/mr/kwin_scripting.po | 109 + po/ms/kcm_kwindesktop.po | 262 ++ po/ms/kcm_kwintabbox.po | 229 + po/ms/kcmkwincompositing.po | 347 ++ po/ms/kcmkwindecoration.po | 204 + po/ms/kcmkwinrules.po | 1341 ++++++ po/ms/kcmkwinscreenedges.po | 238 ++ po/ms/kcmkwm.po | 1476 +++++++ po/ms/kwin.po | 2070 +++++++++ po/ms/kwin_clients.po | 148 + po/ms/kwin_effects.po | 2155 ++++++++++ po/nb/kcm-kwin-scripts.po | 87 + po/nb/kcm_kwindesktop.po | 260 ++ po/nb/kcm_kwintabbox.po | 225 + po/nb/kcmkwincompositing.po | 348 ++ po/nb/kcmkwindecoration.po | 200 + po/nb/kcmkwinrules.po | 1331 ++++++ po/nb/kcmkwinscreenedges.po | 235 ++ po/nb/kcmkwm.po | 1462 +++++++ po/nb/kwin.po | 2063 +++++++++ po/nb/kwin_clients.po | 146 + po/nb/kwin_effects.po | 2148 ++++++++++ po/nb/kwin_scripting.po | 114 + po/nds/kcm-kwin-scripts.po | 89 + po/nds/kcm_kwindesktop.po | 263 ++ po/nds/kcm_kwintabbox.po | 226 + po/nds/kcmkwincompositing.po | 357 ++ po/nds/kcmkwindecoration.po | 210 + po/nds/kcmkwinrules.po | 1332 ++++++ po/nds/kcmkwinscreenedges.po | 246 ++ po/nds/kcmkwm.po | 1472 +++++++ po/nds/kwin.po | 2101 ++++++++++ po/nds/kwin_clients.po | 148 + po/nds/kwin_effects.po | 2198 ++++++++++ po/nds/kwin_scripting.po | 116 + po/ne/kcm_kwindesktop.po | 262 ++ po/ne/kcmkwincompositing.po | 347 ++ po/ne/kcmkwindecoration.po | 209 + po/ne/kcmkwinrules.po | 1337 ++++++ po/ne/kcmkwm.po | 1466 +++++++ po/ne/kwin.po | 2083 +++++++++ po/ne/kwin_clients.po | 152 + po/nl/docs/kcontrol/desktop/index.docbook | 170 + .../kcontrol/kwindecoration/index.docbook | 167 + po/nl/docs/kcontrol/kwineffects/index.docbook | 120 + .../kcontrol/kwinscreenedges/index.docbook | 80 + po/nl/docs/kcontrol/kwintabbox/index.docbook | 157 + .../kcontrol/windowbehaviour/index.docbook | 821 ++++ .../kcontrol/windowspecific/index.docbook | 2195 ++++++++++ po/nl/kcm-kwin-scripts.po | 88 + po/nl/kcm_kwindesktop.po | 262 ++ po/nl/kcm_kwintabbox.po | 225 + po/nl/kcmkwincompositing.po | 363 ++ po/nl/kcmkwindecoration.po | 206 + po/nl/kcmkwinrules.po | 1350 ++++++ po/nl/kcmkwinscreenedges.po | 247 ++ po/nl/kcmkwm.po | 1481 +++++++ po/nl/kwin.po | 2082 +++++++++ po/nl/kwin_clients.po | 147 + po/nl/kwin_effects.po | 2177 ++++++++++ po/nl/kwin_scripting.po | 113 + po/nl/kwin_scripts.po | 61 + po/nn/kcm-kwin-scripts.po | 89 + po/nn/kcm_kwindesktop.po | 262 ++ po/nn/kcm_kwintabbox.po | 227 + po/nn/kcmkwincompositing.po | 357 ++ po/nn/kcmkwindecoration.po | 202 + po/nn/kcmkwinrules.po | 1338 ++++++ po/nn/kcmkwinscreenedges.po | 239 ++ po/nn/kcmkwm.po | 1457 +++++++ po/nn/kwin.po | 2074 +++++++++ po/nn/kwin_clients.po | 147 + po/nn/kwin_effects.po | 2159 ++++++++++ po/nn/kwin_scripting.po | 116 + po/nn/kwin_scripts.po | 63 + po/oc/kcm_kwindesktop.po | 254 ++ po/oc/kcmkwincompositing.po | 336 ++ po/oc/kcmkwindecoration.po | 204 + po/oc/kcmkwinrules.po | 1259 ++++++ po/oc/kcmkwm.po | 1318 ++++++ po/oc/kwin.po | 2017 +++++++++ po/oc/kwin_clients.po | 146 + po/oc/kwin_effects.po | 2153 ++++++++++ po/or/kcm_kwindesktop.po | 253 ++ po/or/kcmkwincompositing.po | 333 ++ po/or/kcmkwindecoration.po | 197 + po/or/kcmkwinrules.po | 1256 ++++++ po/or/kcmkwm.po | 1298 ++++++ po/or/kwin.po | 2000 +++++++++ po/or/kwin_clients.po | 140 + po/or/kwin_effects.po | 2135 ++++++++++ po/pa/kcm-kwin-scripts.po | 87 + po/pa/kcm_kwindesktop.po | 255 ++ po/pa/kcm_kwintabbox.po | 218 + po/pa/kcmkwincompositing.po | 355 ++ po/pa/kcmkwindecoration.po | 204 + po/pa/kcmkwinrules.po | 1258 ++++++ po/pa/kcmkwinscreenedges.po | 239 ++ po/pa/kcmkwm.po | 1336 ++++++ po/pa/kwin.po | 2051 +++++++++ po/pa/kwin_clients.po | 146 + po/pa/kwin_effects.po | 2178 ++++++++++ po/pa/kwin_scripting.po | 109 + po/pl/kcm-kwin-scripts.po | 88 + po/pl/kcm_kwindesktop.po | 262 ++ po/pl/kcm_kwintabbox.po | 227 + po/pl/kcmkwincompositing.po | 359 ++ po/pl/kcmkwindecoration.po | 204 + po/pl/kcmkwinrules.po | 1335 ++++++ po/pl/kcmkwinscreenedges.po | 238 ++ po/pl/kcmkwm.po | 1468 +++++++ po/pl/kwin.po | 2091 +++++++++ po/pl/kwin_clients.po | 146 + po/pl/kwin_effects.po | 2172 ++++++++++ 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 | 83 + po/pt/kcm_kwindesktop.po | 255 ++ po/pt/kcm_kwintabbox.po | 219 + po/pt/kcmkwincompositing.po | 354 ++ po/pt/kcmkwindecoration.po | 194 + po/pt/kcmkwinrules.po | 1338 ++++++ po/pt/kcmkwinscreenedges.po | 235 ++ po/pt/kcmkwm.po | 1470 +++++++ po/pt/kwin.po | 2087 +++++++++ po/pt/kwin_clients.po | 145 + po/pt/kwin_effects.po | 2166 ++++++++++ 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 | 89 + po/pt_BR/kcm_kwindesktop.po | 264 ++ po/pt_BR/kcm_kwintabbox.po | 228 + po/pt_BR/kcmkwincompositing.po | 358 ++ po/pt_BR/kcmkwindecoration.po | 204 + po/pt_BR/kcmkwinrules.po | 1348 ++++++ po/pt_BR/kcmkwinscreenedges.po | 243 ++ po/pt_BR/kcmkwm.po | 1473 +++++++ po/pt_BR/kwin.po | 2093 +++++++++ po/pt_BR/kwin_clients.po | 151 + po/pt_BR/kwin_effects.po | 2169 ++++++++++ po/pt_BR/kwin_scripting.po | 119 + po/pt_BR/kwin_scripts.po | 62 + po/ro/kcm-kwin-scripts.po | 88 + po/ro/kcm_kwindesktop.po | 270 ++ po/ro/kcm_kwintabbox.po | 227 + po/ro/kcmkwincompositing.po | 367 ++ po/ro/kcmkwindecoration.po | 213 + po/ro/kcmkwinrules.po | 1353 ++++++ po/ro/kcmkwinscreenedges.po | 259 ++ po/ro/kcmkwm.po | 1466 +++++++ po/ro/kwin.po | 2067 +++++++++ po/ro/kwin_clients.po | 158 + po/ro/kwin_effects.po | 2202 ++++++++++ po/ro/kwin_scripting.po | 117 + 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 ++++ .../kcontrol/windowspecific/Face-smile.png | Bin 0 -> 1233 bytes .../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 | 94 + po/ru/kcm_kwindesktop.po | 266 ++ po/ru/kcm_kwintabbox.po | 228 + po/ru/kcmkwincompositing.po | 607 +++ po/ru/kcmkwindecoration.po | 211 + po/ru/kcmkwinrules.po | 1347 ++++++ po/ru/kcmkwinscreenedges.po | 245 ++ po/ru/kcmkwm.po | 1466 +++++++ po/ru/kwin.po | 2091 +++++++++ po/ru/kwin_clients.po | 153 + po/ru/kwin_effects.po | 2177 ++++++++++ po/ru/kwin_scripting.po | 119 + po/ru/kwin_scripts.po | 62 + po/se/kcm_kwindesktop.po | 254 ++ po/se/kcmkwincompositing.po | 332 ++ po/se/kcmkwindecoration.po | 196 + po/se/kcmkwinrules.po | 1255 ++++++ po/se/kcmkwm.po | 1334 ++++++ po/se/kwin.po | 2013 +++++++++ po/se/kwin_clients.po | 139 + po/si/kcm_kwindesktop.po | 259 ++ po/si/kcm_kwintabbox.po | 234 ++ po/si/kcmkwincompositing.po | 353 ++ po/si/kcmkwindecoration.po | 208 + po/si/kcmkwinrules.po | 1328 ++++++ po/si/kcmkwinscreenedges.po | 245 ++ po/si/kcmkwm.po | 1463 +++++++ po/si/kwin.po | 2064 +++++++++ po/si/kwin_clients.po | 149 + po/si/kwin_effects.po | 2212 ++++++++++ po/sk/kcm-kwin-scripts.po | 86 + po/sk/kcm_kwindesktop.po | 257 ++ po/sk/kcm_kwintabbox.po | 222 + po/sk/kcmkwincompositing.po | 352 ++ po/sk/kcmkwindecoration.po | 199 + po/sk/kcmkwinrules.po | 1327 ++++++ po/sk/kcmkwinscreenedges.po | 233 ++ po/sk/kcmkwm.po | 1448 +++++++ po/sk/kwin.po | 2075 +++++++++ po/sk/kwin_clients.po | 144 + po/sk/kwin_effects.po | 2155 ++++++++++ po/sk/kwin_scripting.po | 109 + po/sk/kwin_scripts.po | 59 + po/sl/kcm-kwin-scripts.po | 90 + po/sl/kcm_kwindesktop.po | 263 ++ po/sl/kcm_kwintabbox.po | 224 + po/sl/kcmkwincompositing.po | 358 ++ po/sl/kcmkwindecoration.po | 205 + po/sl/kcmkwinrules.po | 1325 ++++++ po/sl/kcmkwinscreenedges.po | 238 ++ po/sl/kcmkwm.po | 1460 +++++++ po/sl/kwin.po | 2088 +++++++++ po/sl/kwin_clients.po | 148 + po/sl/kwin_effects.po | 2196 ++++++++++ po/sl/kwin_scripting.po | 112 + po/sl/kwin_scripts.po | 62 + po/sq/kcm_kwindesktop.po | 254 ++ po/sq/kcmkwincompositing.po | 345 ++ po/sq/kcmkwindecoration.po | 199 + po/sq/kcmkwinrules.po | 1261 ++++++ po/sq/kcmkwinscreenedges.po | 237 ++ po/sq/kcmkwm.po | 1320 ++++++ po/sq/kwin.po | 2021 +++++++++ po/sq/kwin_clients.po | 141 + po/sq/kwin_effects.po | 2177 ++++++++++ po/sr/docs/kcontrol/desktop/index.docbook | 182 + po/sr/kcm-kwin-scripts.po | 89 + po/sr/kcm_kwindesktop.po | 259 ++ po/sr/kcm_kwintabbox.po | 240 ++ po/sr/kcmkwincompositing.po | 364 ++ po/sr/kcmkwindecoration.po | 200 + po/sr/kcmkwinrules.po | 1346 ++++++ po/sr/kcmkwinscreenedges.po | 249 ++ po/sr/kcmkwm.po | 1433 +++++++ po/sr/kwin.po | 2154 ++++++++++ po/sr/kwin_clients.po | 154 + po/sr/kwin_effects.po | 2235 ++++++++++ po/sr/kwin_scripting.po | 111 + po/sr/kwin_scripts.po | 64 + po/sr@ijekavian/kcm-kwin-scripts.po | 89 + po/sr@ijekavian/kcm_kwindesktop.po | 259 ++ po/sr@ijekavian/kcm_kwintabbox.po | 240 ++ po/sr@ijekavian/kcmkwincompositing.po | 364 ++ po/sr@ijekavian/kcmkwindecoration.po | 200 + po/sr@ijekavian/kcmkwinrules.po | 1348 ++++++ po/sr@ijekavian/kcmkwinscreenedges.po | 249 ++ po/sr@ijekavian/kcmkwm.po | 1433 +++++++ po/sr@ijekavian/kwin.po | 2154 ++++++++++ po/sr@ijekavian/kwin_clients.po | 154 + po/sr@ijekavian/kwin_effects.po | 2236 ++++++++++ po/sr@ijekavian/kwin_scripting.po | 111 + po/sr@ijekavian/kwin_scripts.po | 64 + po/sr@ijekavianlatin/kcm-kwin-scripts.po | 89 + po/sr@ijekavianlatin/kcm_kwindesktop.po | 259 ++ po/sr@ijekavianlatin/kcm_kwintabbox.po | 240 ++ po/sr@ijekavianlatin/kcmkwincompositing.po | 364 ++ po/sr@ijekavianlatin/kcmkwindecoration.po | 200 + po/sr@ijekavianlatin/kcmkwinrules.po | 1349 ++++++ po/sr@ijekavianlatin/kcmkwinscreenedges.po | 249 ++ po/sr@ijekavianlatin/kcmkwm.po | 1435 +++++++ po/sr@ijekavianlatin/kwin.po | 2155 ++++++++++ po/sr@ijekavianlatin/kwin_clients.po | 155 + po/sr@ijekavianlatin/kwin_effects.po | 2238 ++++++++++ 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_kwindesktop.po | 259 ++ po/sr@latin/kcm_kwintabbox.po | 240 ++ po/sr@latin/kcmkwincompositing.po | 364 ++ po/sr@latin/kcmkwindecoration.po | 200 + po/sr@latin/kcmkwinrules.po | 1347 ++++++ po/sr@latin/kcmkwinscreenedges.po | 249 ++ po/sr@latin/kcmkwm.po | 1435 +++++++ po/sr@latin/kwin.po | 2155 ++++++++++ po/sr@latin/kwin_clients.po | 154 + po/sr@latin/kwin_effects.po | 2236 ++++++++++ po/sr@latin/kwin_scripting.po | 111 + po/sr@latin/kwin_scripts.po | 64 + po/sv/docs/kcontrol/desktop/index.docbook | 170 + .../kcontrol/kwindecoration/index.docbook | 183 + po/sv/docs/kcontrol/kwineffects/index.docbook | 134 + .../kcontrol/kwinscreenedges/index.docbook | 94 + po/sv/docs/kcontrol/kwintabbox/index.docbook | 177 + .../kcontrol/windowbehaviour/index.docbook | 835 ++++ .../kcontrol/windowspecific/index.docbook | 2241 ++++++++++ po/sv/kcm-kwin-scripts.po | 87 + po/sv/kcm_kwindesktop.po | 261 ++ po/sv/kcm_kwintabbox.po | 221 + po/sv/kcmkwincompositing.po | 355 ++ po/sv/kcmkwindecoration.po | 198 + po/sv/kcmkwinrules.po | 1330 ++++++ po/sv/kcmkwinscreenedges.po | 239 ++ po/sv/kcmkwm.po | 1459 +++++++ po/sv/kwin.po | 2068 +++++++++ po/sv/kwin_clients.po | 144 + po/sv/kwin_effects.po | 2155 ++++++++++ po/sv/kwin_scripting.po | 113 + po/sv/kwin_scripts.po | 61 + po/ta/kcm_kwindesktop.po | 264 ++ po/ta/kcmkwincompositing.po | 347 ++ po/ta/kcmkwindecoration.po | 207 + po/ta/kcmkwinrules.po | 1309 ++++++ po/ta/kcmkwm.po | 1427 +++++++ po/ta/kwin.po | 2068 +++++++++ po/ta/kwin_clients.po | 143 + po/ta/kwin_effects.po | 2225 ++++++++++ po/te/kcm_kwindesktop.po | 262 ++ po/te/kcmkwincompositing.po | 369 ++ po/te/kcmkwindecoration.po | 208 + po/te/kcmkwinrules.po | 1272 ++++++ po/te/kcmkwm.po | 1384 ++++++ po/te/kwin.po | 2013 +++++++++ po/te/kwin_clients.po | 144 + po/te/kwin_effects.po | 2162 ++++++++++ po/tg/kcm_kwindesktop.po | 256 ++ po/tg/kcmkwincompositing.po | 350 ++ po/tg/kcmkwindecoration.po | 209 + po/tg/kcmkwinrules.po | 1305 ++++++ po/tg/kcmkwm.po | 1482 +++++++ po/tg/kwin.po | 2075 +++++++++ po/tg/kwin_clients.po | 142 + po/tg/kwin_effects.po | 2207 ++++++++++ po/th/kcm_kwindesktop.po | 260 ++ po/th/kcm_kwintabbox.po | 235 ++ po/th/kcmkwincompositing.po | 351 ++ po/th/kcmkwindecoration.po | 202 + po/th/kcmkwinrules.po | 1331 ++++++ po/th/kcmkwinscreenedges.po | 248 ++ po/th/kcmkwm.po | 1459 +++++++ po/th/kwin.po | 2067 +++++++++ po/th/kwin_clients.po | 154 + po/th/kwin_effects.po | 2205 ++++++++++ po/tr/kcm-kwin-scripts.po | 88 + po/tr/kcm_kwindesktop.po | 263 ++ po/tr/kcm_kwintabbox.po | 226 + po/tr/kcmkwincompositing.po | 360 ++ po/tr/kcmkwindecoration.po | 202 + po/tr/kcmkwinrules.po | 1334 ++++++ po/tr/kcmkwinscreenedges.po | 249 ++ po/tr/kcmkwm.po | 1459 +++++++ po/tr/kwin.po | 2087 +++++++++ po/tr/kwin_clients.po | 150 + po/tr/kwin_effects.po | 2202 ++++++++++ po/tr/kwin_scripting.po | 115 + po/tr/kwin_scripts.po | 61 + po/ug/kcm-kwin-scripts.po | 85 + po/ug/kcm_kwindesktop.po | 252 ++ po/ug/kcm_kwintabbox.po | 218 + po/ug/kcmkwincompositing.po | 344 ++ po/ug/kcmkwindecoration.po | 206 + po/ug/kcmkwinrules.po | 1259 ++++++ po/ug/kcmkwinscreenedges.po | 235 ++ po/ug/kcmkwm.po | 1340 ++++++ po/ug/kwin.po | 2041 +++++++++ po/ug/kwin_clients.po | 150 + po/ug/kwin_effects.po | 2192 ++++++++++ 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 | 134 + .../kcontrol/kwinscreenedges/index.docbook | 94 + po/uk/docs/kcontrol/kwintabbox/index.docbook | 171 + .../kcontrol/windowbehaviour/index.docbook | 831 ++++ .../kcontrol/windowspecific/index.docbook | 2225 ++++++++++ po/uk/kcm-kwin-scripts.po | 90 + po/uk/kcm_kwindesktop.po | 264 ++ po/uk/kcm_kwintabbox.po | 225 + po/uk/kcmkwincompositing.po | 362 ++ po/uk/kcmkwindecoration.po | 203 + po/uk/kcmkwinrules.po | 1340 ++++++ po/uk/kcmkwinscreenedges.po | 238 ++ po/uk/kcmkwm.po | 1465 +++++++ po/uk/kwin.po | 2085 +++++++++ po/uk/kwin_clients.po | 149 + po/uk/kwin_effects.po | 2175 ++++++++++ po/uk/kwin_scripting.po | 116 + po/uk/kwin_scripts.po | 64 + po/uz/kcmkwindecoration.po | 208 + po/uz/kcmkwinrules.po | 1265 ++++++ po/uz/kcmkwm.po | 1395 ++++++ po/uz/kwin.po | 2061 +++++++++ po/uz/kwin_clients.po | 144 + po/uz@cyrillic/kcmkwindecoration.po | 208 + po/uz@cyrillic/kcmkwinrules.po | 1265 ++++++ po/uz@cyrillic/kcmkwm.po | 1393 ++++++ po/uz@cyrillic/kwin.po | 2061 +++++++++ po/uz@cyrillic/kwin_clients.po | 144 + po/wa/kcm_kwindesktop.po | 258 ++ po/wa/kcm_kwintabbox.po | 236 ++ po/wa/kcmkwincompositing.po | 351 ++ po/wa/kcmkwindecoration.po | 203 + po/wa/kcmkwinrules.po | 1280 ++++++ po/wa/kcmkwinscreenedges.po | 251 ++ po/wa/kcmkwm.po | 1439 +++++++ po/wa/kwin.po | 2049 +++++++++ po/wa/kwin_clients.po | 151 + po/wa/kwin_effects.po | 2207 ++++++++++ po/xh/kcmkwindecoration.po | 197 + po/xh/kcmkwm.po | 1405 +++++++ po/xh/kwin.po | 2030 +++++++++ po/zh_CN/kcm-kwin-scripts.po | 92 + po/zh_CN/kcm_kwindesktop.po | 262 ++ po/zh_CN/kcm_kwintabbox.po | 224 + po/zh_CN/kcmkwincompositing.po | 348 ++ po/zh_CN/kcmkwindecoration.po | 206 + po/zh_CN/kcmkwinrules.po | 1308 ++++++ po/zh_CN/kcmkwinscreenedges.po | 240 ++ po/zh_CN/kcmkwm.po | 1401 +++++++ po/zh_CN/kwin.po | 2049 +++++++++ po/zh_CN/kwin_clients.po | 143 + po/zh_CN/kwin_effects.po | 2154 ++++++++++ po/zh_CN/kwin_scripting.po | 113 + po/zh_CN/kwin_scripts.po | 64 + po/zh_TW/kcm-kwin-scripts.po | 89 + po/zh_TW/kcm_kwindesktop.po | 256 ++ po/zh_TW/kcm_kwintabbox.po | 220 + po/zh_TW/kcmkwincompositing.po | 348 ++ po/zh_TW/kcmkwindecoration.po | 200 + po/zh_TW/kcmkwinrules.po | 1308 ++++++ po/zh_TW/kcmkwinscreenedges.po | 233 ++ po/zh_TW/kcmkwm.po | 1389 ++++++ po/zh_TW/kwin.po | 2044 +++++++++ po/zh_TW/kwin_clients.po | 144 + po/zh_TW/kwin_effects.po | 2147 ++++++++++ po/zh_TW/kwin_scripting.po | 110 + po/zh_TW/kwin_scripts.po | 61 + pointer_input.cpp | 1369 ++++++ pointer_input.h | 250 ++ popup_input_filter.cpp | 88 + popup_input_filter.h | 50 + qml/CMakeLists.txt | 3 + .../plasma/dummydata/osd.qml | 7 + qml/onscreennotification/plasma/main.qml | 47 + qml/outline/plasma/outline.qml | 135 + qml/virtualkeyboard/main.qml | 32 + resources.qrc | 7 + rootinfo_filter.cpp | 45 + rootinfo_filter.h | 42 + rules.cpp | 1178 ++++++ rules.h | 396 ++ sc-apps-kwin.svgz | Bin 0 -> 3106 bytes scene.cpp | 1148 +++++ scene.h | 682 +++ screenedge.cpp | 1494 +++++++ screenedge.h | 590 +++ screenlockerwatcher.cpp | 133 + screenlockerwatcher.h | 60 + screens.cpp | 320 ++ screens.h | 278 ++ scripting/CMakeLists.txt | 10 + scripting/Messages.sh | 2 + scripting/dbuscall.cpp | 53 + scripting/dbuscall.h | 147 + scripting/documentation-effect-global.xml | 179 + scripting/documentation-global.xml | 144 + scripting/genericscriptedconfig.cpp | 176 + scripting/genericscriptedconfig.h | 99 + scripting/genericscriptedconfig.json | 7 + scripting/kwinscript.desktop | 67 + scripting/meta.cpp | 240 ++ scripting/meta.h | 127 + scripting/screenedgeitem.cpp | 118 + scripting/screenedgeitem.h | 129 + scripting/scriptedeffect.cpp | 686 +++ scripting/scriptedeffect.h | 134 + scripting/scripting.cpp | 878 ++++ scripting/scripting.h | 431 ++ scripting/scripting_logging.cpp | 21 + scripting/scripting_logging.h | 26 + scripting/scripting_model.cpp | 923 ++++ scripting/scripting_model.h | 380 ++ scripting/scriptingutils.cpp | 46 + scripting/scriptingutils.h | 369 ++ scripting/timer.cpp | 43 + scripting/workspace_wrapper.cpp | 366 ++ scripting/workspace_wrapper.h | 379 ++ scripts/CMakeLists.txt | 13 + scripts/Messages.sh | 4 + scripts/desktopchangeosd/contents/ui/main.qml | 37 + scripts/desktopchangeosd/contents/ui/osd.qml | 304 ++ scripts/desktopchangeosd/metadata.desktop | 111 + scripts/enforcedeco/contents/code/main.js | 38 + scripts/enforcedeco/metadata.desktop | 97 + scripts/minimizeall/contents/code/main.js | 73 + scripts/minimizeall/metadata.desktop | 92 + .../contents/code/main.js | 34 + .../synchronizeskipswitcher/metadata.desktop | 104 + scripts/videowall/contents/code/main.js | 46 + scripts/videowall/contents/config/main.xml | 22 + scripts/videowall/contents/ui/config.ui | 148 + scripts/videowall/metadata.desktop | 111 + settings.kcfgc | 7 + shaders/1.10/lanczos-fragment.glsl | 16 + shaders/1.40/lanczos-fragment.glsl | 19 + shadow.cpp | 441 ++ shadow.h | 193 + shell_client.cpp | 1717 ++++++++ shell_client.h | 270 ++ shortcutdialog.ui | 104 + sm.cpp | 536 +++ sm.h | 103 + tabbox/CMakeLists.txt | 3 + tabbox/clientmodel.cpp | 263 ++ tabbox/clientmodel.h | 113 + tabbox/desktopchain.cpp | 142 + tabbox/desktopchain.h | 148 + tabbox/desktopmodel.cpp | 183 + tabbox/desktopmodel.h | 89 + tabbox/kwindesktopswitcher.desktop | 58 + tabbox/kwinwindowswitcher.desktop | 60 + tabbox/switcheritem.cpp | 115 + tabbox/switcheritem.h | 118 + tabbox/tabbox.cpp | 1614 +++++++ tabbox/tabbox.h | 303 ++ tabbox/tabbox_logging.cpp | 21 + tabbox/tabbox_logging.h | 26 + tabbox/tabboxconfig.cpp | 209 + tabbox/tabboxconfig.h | 326 ++ tabbox/tabboxhandler.cpp | 659 +++ tabbox/tabboxhandler.h | 408 ++ tabbox/x11_filter.cpp | 162 + tabbox/x11_filter.h | 49 + tabgroup.cpp | 360 ++ tabgroup.h | 209 + tabletmodemanager.cpp | 103 + tabletmodemanager.h | 60 + tests/CMakeLists.txt | 56 + tests/cursorhotspottest.cpp | 157 + tests/inputmethodstest.qml | 84 + tests/libinputtest.cpp | 117 + tests/normalhintsbasesizetest.cpp | 117 + tests/orientationtest.cpp | 62 + tests/pointerconstraintstest.cpp | 417 ++ tests/pointerconstraintstest.h | 178 + tests/pointerconstraintstest.qml | 219 + tests/pointergesturestest.cpp | 168 + tests/pointergesturestest.qml | 30 + tests/screenedgeshowtest.cpp | 361 ++ tests/unmapdestroytest.qml | 86 + tests/waylandclienttest.cpp | 271 ++ tests/waylandclienttest.h | 74 + tests/x11shadowreader.cpp | 130 + thumbnailitem.cpp | 227 + thumbnailitem.h | 149 + toplevel.cpp | 557 +++ toplevel.h | 855 ++++ touch_input.cpp | 203 + touch_input.h | 92 + udev.cpp | 323 ++ udev.h | 102 + unmanaged.cpp | 164 + unmanaged.h | 63 + useractions.cpp | 1829 ++++++++ useractions.h | 288 ++ utils.cpp | 200 + utils.h | 242 ++ virtual_terminal.cpp | 215 + virtual_terminal.h | 61 + virtualdesktops.cpp | 651 +++ virtualdesktops.h | 693 +++ virtualkeyboard.cpp | 443 ++ virtualkeyboard.h | 71 + virtualkeyboard_dbus.cpp | 37 + virtualkeyboard_dbus.h | 62 + was_user_interaction_x11_filter.cpp | 38 + was_user_interaction_x11_filter.h | 37 + wayland_cursor_theme.cpp | 117 + wayland_cursor_theme.h | 64 + wayland_server.cpp | 777 ++++ wayland_server.h | 273 ++ window_property_notify_x11_filter.cpp | 51 + window_property_notify_x11_filter.h | 42 + workspace.cpp | 1754 ++++++++ workspace.h | 770 ++++ x11eventfilter.cpp | 61 + x11eventfilter.h | 87 + xcbutils.cpp | 620 +++ xcbutils.h | 1887 +++++++++ xkb.cpp | 553 +++ xkb.h | 168 + xkb_qt_mapping.h | 317 ++ 2405 files changed, 1007600 insertions(+) create mode 100644 .arcconfig create mode 100644 16-apps-kwin.png create mode 100644 32-apps-kwin.png create mode 100644 48-apps-kwin.png create mode 100644 CMakeLists.txt create mode 100644 COMPLIANCE create mode 100644 CONFIGURING create mode 100644 COPYING create mode 100644 COPYING.DOC create mode 100644 ExtraDesktop.sh create mode 100644 HACKING create mode 100644 KWinDBusInterfaceConfig.cmake.in create mode 100644 Mainpage.dox create mode 100644 Messages.sh create mode 100644 README 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 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/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/activities_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/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_useractions_menu.cpp create mode 100644 autotests/integration/effects/CMakeLists.txt create mode 100644 autotests/integration/effects/fade_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/effectContext.js create mode 100644 autotests/integration/effects/scripts/effectsHandler.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/slidingpopups_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/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_xdg_runtime_dir_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/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/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/shell_client_rules_test.cpp create mode 100644 autotests/integration/shell_client_test.cpp create mode 100644 autotests/integration/showing_desktop_test.cpp create mode 100644 autotests/integration/start_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_no_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/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/xclipboardsync_test.cpp create mode 100644 autotests/integration/xwayland_input_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-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-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/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_client.cpp create mode 100644 autotests/mock_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/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/test_xrandr_screens.cpp create mode 100644 autotests/testutils.h create mode 100644 autotests/workspace.h create mode 100644 client.cpp create mode 100644 client.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/FindQt5EventDispatcherSupport.cmake create mode 100644 cmake/modules/FindQt5FontDatabaseSupport.cmake create mode 100644 cmake/modules/FindQt5PlatformSupport.cmake create mode 100644 cmake/modules/FindQt5ThemeSupport.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/Findepoxy.cmake create mode 100644 cmake/modules/Findgbm.cmake create mode 100644 cmake/modules/Findlibhybris.cmake 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/gammaramp.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/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 doc/CMakeLists.txt 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/configure-filter.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/cubeslide.cpp create mode 100644 effects/cube/cubeslide.h create mode 100644 effects/cube/cubeslide.kcfg create mode 100644 effects/cube/cubeslide_config.cpp create mode 100644 effects/cube/cubeslide_config.desktop create mode 100644 effects/cube/cubeslide_config.h create mode 100644 effects/cube/cubeslide_config.ui create mode 100644 effects/cube/cubeslideconfig.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/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/CMakeLists.txt create mode 100644 effects/dialogparent/package/CMakeLists.txt 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/CMakeLists.txt create mode 100644 effects/dimscreen/dimscreen.cpp create mode 100644 effects/dimscreen/dimscreen.h create mode 100644 effects/effect_builtins.cpp create mode 100644 effects/effect_builtins.h create mode 100644 effects/eyeonscreen/CMakeLists.txt create mode 100644 effects/eyeonscreen/package/CMakeLists.txt 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/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/CMakeLists.txt create mode 100644 effects/frozenapp/package/CMakeLists.txt create mode 100644 effects/frozenapp/package/contents/code/main.js create mode 100644 effects/frozenapp/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/CMakeLists.txt create mode 100644 effects/login/package/CMakeLists.txt 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/CMakeLists.txt create mode 100644 effects/logout/package/CMakeLists.txt 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/CMakeLists.txt create mode 100644 effects/maximize/package/CMakeLists.txt create mode 100644 effects/maximize/package/contents/code/maximize.js create mode 100644 effects/maximize/package/metadata.desktop create mode 100644 effects/minimizeanimation/CMakeLists.txt create mode 100644 effects/minimizeanimation/minimizeanimation.cpp create mode 100644 effects/minimizeanimation/minimizeanimation.h create mode 100644 effects/morphingpopups/CMakeLists.txt create mode 100644 effects/morphingpopups/package/CMakeLists.txt 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 100755 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/CMakeLists.txt create mode 100644 effects/scale/scale.cpp create mode 100644 effects/scale/scale.h create mode 100644 effects/scale/scale.kcfg create mode 100644 effects/scale/scale_config.cpp create mode 100644 effects/scale/scale_config.desktop create mode 100644 effects/scale/scale_config.h create mode 100644 effects/scale/scale_config.ui create mode 100644 effects/scale/scaleconfig.kcfgc 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/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/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/startupfeedback/CMakeLists.txt create mode 100644 effects/startupfeedback/data/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/CMakeLists.txt create mode 100644 effects/translucency/package/CMakeLists.txt 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/CMakeLists.txt create mode 100644 effects/windowaperture/package/CMakeLists.txt 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/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 geometry.cpp 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 helpers/xclipboardsync/CMakeLists.txt create mode 100644 helpers/xclipboardsync/main.cpp create mode 100644 helpers/xclipboardsync/waylandclipboard.cpp create mode 100644 helpers/xclipboardsync/waylandclipboard.h 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 kcmkwin/CMakeLists.txt create mode 100644 kcmkwin/kwincompositing/CMakeLists.txt create mode 100644 kcmkwin/kwincompositing/Messages.sh create mode 100644 kcmkwin/kwincompositing/compositing.cpp create mode 100644 kcmkwin/kwincompositing/compositing.h create mode 100644 kcmkwin/kwincompositing/compositing.ui create mode 100644 kcmkwin/kwincompositing/config-compiler.h.cmake create mode 100644 kcmkwin/kwincompositing/config-prefix.h.cmake create mode 100644 kcmkwin/kwincompositing/effectconfig.cpp create mode 100644 kcmkwin/kwincompositing/effectconfig.h create mode 100644 kcmkwin/kwincompositing/kcmkwineffects.desktop create mode 100644 kcmkwin/kwincompositing/kwincompositing.desktop create mode 100644 kcmkwin/kwincompositing/kwineffect.knsrc create mode 100644 kcmkwin/kwincompositing/main.cpp create mode 100644 kcmkwin/kwincompositing/model.cpp create mode 100644 kcmkwin/kwincompositing/model.h create mode 100644 kcmkwin/kwincompositing/qml/Effect.qml create mode 100644 kcmkwin/kwincompositing/qml/EffectView.qml create mode 100644 kcmkwin/kwincompositing/qml/Video.qml create mode 100644 kcmkwin/kwincompositing/qml/main.qml create mode 100644 kcmkwin/kwincompositing/test/effectmodeltest.cpp create mode 100644 kcmkwin/kwincompositing/test/effectmodeltest.h create mode 100644 kcmkwin/kwincompositing/test/modeltest.cpp create mode 100644 kcmkwin/kwincompositing/test/modeltest.h 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/kcm.ui create mode 100644 kcmkwin/kwindecoration/kwindecoration.desktop create mode 100644 kcmkwin/kwindecoration/qml/ButtonGroup.qml create mode 100644 kcmkwin/kwindecoration/qml/Buttons.qml create mode 100644 kcmkwin/kwindecoration/qml/Previews.qml create mode 100644 kcmkwin/kwindecoration/qml/main.qml create mode 100644 kcmkwin/kwindesktop/CMakeLists.txt create mode 100644 kcmkwin/kwindesktop/Messages.sh create mode 100644 kcmkwin/kwindesktop/desktop.desktop create mode 100644 kcmkwin/kwindesktop/desktopnameswidget.cpp create mode 100644 kcmkwin/kwindesktop/desktopnameswidget.h create mode 100644 kcmkwin/kwindesktop/main.cpp create mode 100644 kcmkwin/kwindesktop/main.h create mode 100644 kcmkwin/kwindesktop/main.ui 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/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/detectwidget.cpp create mode 100644 kcmkwin/kwinrules/detectwidget.h create mode 100644 kcmkwin/kwinrules/detectwidget.ui create mode 100644 kcmkwin/kwinrules/editshortcut.ui create mode 100644 kcmkwin/kwinrules/kcm.cpp create mode 100644 kcmkwin/kwinrules/kcm.h create mode 100644 kcmkwin/kwinrules/kwinrules.desktop create mode 100644 kcmkwin/kwinrules/kwinsrc.cpp create mode 100644 kcmkwin/kwinrules/main.cpp create mode 100644 kcmkwin/kwinrules/ruleslist.cpp create mode 100644 kcmkwin/kwinrules/ruleslist.h create mode 100644 kcmkwin/kwinrules/ruleslist.ui create mode 100644 kcmkwin/kwinrules/ruleswidget.cpp create mode 100644 kcmkwin/kwinrules/ruleswidget.h create mode 100644 kcmkwin/kwinrules/ruleswidgetbase.ui create mode 100644 kcmkwin/kwinrules/yesnobox.h create mode 100644 kcmkwin/kwinscreenedges/CMakeLists.txt create mode 100644 kcmkwin/kwinscreenedges/Messages.sh create mode 100644 kcmkwin/kwinscreenedges/kwinscreenedges.desktop create mode 100644 kcmkwin/kwinscreenedges/kwintouchscreen.desktop 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/kwinswitcher.knsrc create mode 100644 kcmkwin/kwintabbox/kwintabbox.desktop 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.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 lanczosfilter.cpp create mode 100644 lanczosfilter.h create mode 100644 layers.cpp 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/kwineffects.cpp create mode 100644 libkwineffects/kwineffects.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 logind.cpp create mode 100644 logind.h 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 manage.cpp 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.freedesktop.ScreenSaver.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 org.kde.kwin.OrientationSensor.xml create mode 100644 orientation_sensor.cpp create mode 100644 orientation_sensor.h 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 packageplugins/CMakeLists.txt create mode 100644 packageplugins/aurorae/CMakeLists.txt create mode 100644 packageplugins/aurorae/aurorae.cpp create mode 100644 packageplugins/aurorae/aurorae.h create mode 100644 packageplugins/aurorae/kwin-packagestructure-aurorae.desktop create mode 100644 packageplugins/decoration/CMakeLists.txt create mode 100644 packageplugins/decoration/decoration.cpp create mode 100644 packageplugins/decoration/decoration.h create mode 100644 packageplugins/decoration/kwin-packagestructure-decoration.desktop create mode 100644 packageplugins/scripts/CMakeLists.txt create mode 100644 packageplugins/scripts/kwin-packagestructure-scripts.desktop create mode 100644 packageplugins/scripts/scripts.cpp create mode 100644 packageplugins/scripts/scripts.h create mode 100644 packageplugins/windowswitcher/CMakeLists.txt create mode 100644 packageplugins/windowswitcher/kwin-packagestructure-windowswitcher.desktop create mode 100644 packageplugins/windowswitcher/windowswitcher.cpp create mode 100644 packageplugins/windowswitcher/windowswitcher.h create mode 100644 placement.cpp create mode 100644 placement.h 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/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/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/egl_gbm_backend.cpp create mode 100644 plugins/platforms/drm/egl_gbm_backend.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/remoteaccess_manager.cpp create mode 100644 plugins/platforms/drm/remoteaccess_manager.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/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/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/sync_filter.cpp create mode 100644 plugins/platforms/x11/standalone/sync_filter.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_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/qpa/CMakeLists.txt create mode 100644 plugins/qpa/abstractplatformcontext.cpp create mode 100644 plugins/qpa/abstractplatformcontext.h create mode 100644 plugins/qpa/backingstore.cpp create mode 100644 plugins/qpa/backingstore.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/nativeinterface.cpp create mode 100644 plugins/qpa/nativeinterface.h create mode 100644 plugins/qpa/platformcontextwayland.cpp create mode 100644 plugins/qpa/platformcontextwayland.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/sharingplatformcontext.cpp create mode 100644 plugins/qpa/sharingplatformcontext.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/opengl.json create mode 100644 plugins/scenes/opengl/scene_opengl.cpp create mode 100644 plugins/scenes/opengl/scene_opengl.h 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 po/af/kcmkwindecoration.po create mode 100644 po/af/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ar/kcm_kwintabbox.po create mode 100644 po/ar/kcmkwincompositing.po create mode 100644 po/ar/kcmkwindecoration.po create mode 100644 po/ar/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ast/kcm_kwintabbox.po create mode 100644 po/ast/kcmkwincompositing.po create mode 100644 po/ast/kcmkwindecoration.po create mode 100644 po/ast/kcmkwinrules.po create mode 100644 po/ast/kcmkwinscreenedges.po create mode 100644 po/ast/kcmkwm.po create mode 100644 po/ast/kwin.po create mode 100644 po/ast/kwin_clients.po create mode 100644 po/ast/kwin_effects.po create mode 100644 po/ast/kwin_scripting.po create mode 100644 po/ast/kwin_scripts.po create mode 100644 po/be/kcm_kwindesktop.po create mode 100644 po/be/kcmkwincompositing.po create mode 100644 po/be/kcmkwindecoration.po create mode 100644 po/be/kcmkwinrules.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_kwindesktop.po create mode 100644 po/bg/kcm_kwintabbox.po create mode 100644 po/bg/kcmkwincompositing.po create mode 100644 po/bg/kcmkwindecoration.po create mode 100644 po/bg/kcmkwinrules.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_kwindesktop.po create mode 100644 po/bn_IN/kcmkwindecoration.po create mode 100644 po/bn_IN/kcmkwinrules.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/kcmkwindecoration.po create mode 100644 po/br/kcmkwinrules.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_kwindesktop.po create mode 100644 po/bs/kcm_kwintabbox.po create mode 100644 po/bs/kcmkwincompositing.po create mode 100644 po/bs/kcmkwindecoration.po create mode 100644 po/bs/kcmkwinrules.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/configure.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/configure-effects.png create mode 100644 po/ca/docs/kcontrol/kwineffects/configure-filter.png create mode 100644 po/ca/docs/kcontrol/kwineffects/dialog-information.png create mode 100644 po/ca/docs/kcontrol/kwineffects/index.docbook create mode 100644 po/ca/docs/kcontrol/kwineffects/video.png 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/Face-smile.png create mode 100644 po/ca/docs/kcontrol/windowspecific/index.docbook create mode 100644 po/ca/docs/kcontrol/windowspecific/pager-4-desktops.png create mode 100644 po/ca/kcm-kwin-scripts.po create mode 100644 po/ca/kcm_kwindesktop.po create mode 100644 po/ca/kcm_kwintabbox.po create mode 100644 po/ca/kcmkwincompositing.po create mode 100644 po/ca/kcmkwindecoration.po create mode 100644 po/ca/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ca@valencia/kcm_kwintabbox.po create mode 100644 po/ca@valencia/kcmkwincompositing.po create mode 100644 po/ca@valencia/kcmkwindecoration.po create mode 100644 po/ca@valencia/kcmkwinrules.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_kwindesktop.po create mode 100644 po/cs/kcm_kwintabbox.po create mode 100644 po/cs/kcmkwincompositing.po create mode 100644 po/cs/kcmkwindecoration.po create mode 100644 po/cs/kcmkwinrules.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_kwindesktop.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/kcmkwindecoration.po create mode 100644 po/cy/kcmkwinrules.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_kwindesktop.po create mode 100644 po/da/kcm_kwintabbox.po create mode 100644 po/da/kcmkwincompositing.po create mode 100644 po/da/kcmkwindecoration.po create mode 100644 po/da/kcmkwinrules.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_kwindesktop.po create mode 100644 po/de/kcm_kwintabbox.po create mode 100644 po/de/kcmkwincompositing.po create mode 100644 po/de/kcmkwindecoration.po create mode 100644 po/de/kcmkwinrules.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_kwindesktop.po create mode 100644 po/el/kcm_kwintabbox.po create mode 100644 po/el/kcmkwincompositing.po create mode 100644 po/el/kcmkwindecoration.po create mode 100644 po/el/kcmkwinrules.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_kwindesktop.po create mode 100644 po/en_GB/kcm_kwintabbox.po create mode 100644 po/en_GB/kcmkwincompositing.po create mode 100644 po/en_GB/kcmkwindecoration.po create mode 100644 po/en_GB/kcmkwinrules.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_kwindesktop.po create mode 100644 po/eo/kcm_kwintabbox.po create mode 100644 po/eo/kcmkwincompositing.po create mode 100644 po/eo/kcmkwindecoration.po create mode 100644 po/eo/kcmkwinrules.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_kwindesktop.po create mode 100644 po/es/kcm_kwintabbox.po create mode 100644 po/es/kcmkwincompositing.po create mode 100644 po/es/kcmkwindecoration.po create mode 100644 po/es/kcmkwinrules.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_kwindesktop.po create mode 100644 po/et/kcm_kwintabbox.po create mode 100644 po/et/kcmkwincompositing.po create mode 100644 po/et/kcmkwindecoration.po create mode 100644 po/et/kcmkwinrules.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_kwindesktop.po create mode 100644 po/eu/kcm_kwintabbox.po create mode 100644 po/eu/kcmkwincompositing.po create mode 100644 po/eu/kcmkwindecoration.po create mode 100644 po/eu/kcmkwinrules.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_kwindesktop.po create mode 100644 po/fa/kcm_kwintabbox.po create mode 100644 po/fa/kcmkwincompositing.po create mode 100644 po/fa/kcmkwindecoration.po create mode 100644 po/fa/kcmkwinrules.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_kwindesktop.po create mode 100644 po/fi/kcm_kwintabbox.po create mode 100644 po/fi/kcmkwincompositing.po create mode 100644 po/fi/kcmkwindecoration.po create mode 100644 po/fi/kcmkwinrules.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/kcm-kwin-scripts.po create mode 100644 po/fr/kcm_kwindesktop.po create mode 100644 po/fr/kcm_kwintabbox.po create mode 100644 po/fr/kcmkwincompositing.po create mode 100644 po/fr/kcmkwindecoration.po create mode 100644 po/fr/kcmkwinrules.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_kwindesktop.po create mode 100644 po/fy/kcmkwincompositing.po create mode 100644 po/fy/kcmkwindecoration.po create mode 100644 po/fy/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ga/kcm_kwintabbox.po create mode 100644 po/ga/kcmkwincompositing.po create mode 100644 po/ga/kcmkwindecoration.po create mode 100644 po/ga/kcmkwinrules.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_kwindesktop.po create mode 100644 po/gl/kcm_kwintabbox.po create mode 100644 po/gl/kcmkwincompositing.po create mode 100644 po/gl/kcmkwindecoration.po create mode 100644 po/gl/kcmkwinrules.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_kwindesktop.po create mode 100644 po/gu/kcm_kwintabbox.po create mode 100644 po/gu/kcmkwincompositing.po create mode 100644 po/gu/kcmkwindecoration.po create mode 100644 po/gu/kcmkwinrules.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_kwindesktop.po create mode 100644 po/he/kcm_kwintabbox.po create mode 100644 po/he/kcmkwincompositing.po create mode 100644 po/he/kcmkwindecoration.po create mode 100644 po/he/kcmkwinrules.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_kwindesktop.po create mode 100644 po/hi/kcm_kwintabbox.po create mode 100644 po/hi/kcmkwincompositing.po create mode 100644 po/hi/kcmkwindecoration.po create mode 100644 po/hi/kcmkwinrules.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_kwindesktop.po create mode 100644 po/hne/kcmkwincompositing.po create mode 100644 po/hne/kcmkwindecoration.po create mode 100644 po/hne/kcmkwinrules.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_kwindesktop.po create mode 100644 po/hr/kcm_kwintabbox.po create mode 100644 po/hr/kcmkwincompositing.po create mode 100644 po/hr/kcmkwindecoration.po create mode 100644 po/hr/kcmkwinrules.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_kwindesktop.po create mode 100644 po/hsb/kcmkwincompositing.po create mode 100644 po/hsb/kcmkwindecoration.po create mode 100644 po/hsb/kcmkwinrules.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_kwindesktop.po create mode 100644 po/hu/kcm_kwintabbox.po create mode 100644 po/hu/kcmkwincompositing.po create mode 100644 po/hu/kcmkwindecoration.po create mode 100644 po/hu/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ia/kcm_kwintabbox.po create mode 100644 po/ia/kcmkwincompositing.po create mode 100644 po/ia/kcmkwindecoration.po create mode 100644 po/ia/kcmkwinrules.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/kcm-kwin-scripts.po create mode 100644 po/id/kcm_kwindesktop.po create mode 100644 po/id/kcm_kwintabbox.po create mode 100644 po/id/kcmkwincompositing.po create mode 100644 po/id/kcmkwindecoration.po create mode 100644 po/id/kcmkwinrules.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_kwindesktop.po create mode 100644 po/is/kcm_kwintabbox.po create mode 100644 po/is/kcmkwincompositing.po create mode 100644 po/is/kcmkwindecoration.po create mode 100644 po/is/kcmkwinrules.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_kwindesktop.po create mode 100644 po/it/kcm_kwintabbox.po create mode 100644 po/it/kcmkwincompositing.po create mode 100644 po/it/kcmkwindecoration.po create mode 100644 po/it/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ja/kcm_kwintabbox.po create mode 100644 po/ja/kcmkwincompositing.po create mode 100644 po/ja/kcmkwindecoration.po create mode 100644 po/ja/kcmkwinrules.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_kwindesktop.po create mode 100644 po/kk/kcm_kwintabbox.po create mode 100644 po/kk/kcmkwincompositing.po create mode 100644 po/kk/kcmkwindecoration.po create mode 100644 po/kk/kcmkwinrules.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_kwindesktop.po create mode 100644 po/km/kcm_kwintabbox.po create mode 100644 po/km/kcmkwincompositing.po create mode 100644 po/km/kcmkwindecoration.po create mode 100644 po/km/kcmkwinrules.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_kwindesktop.po create mode 100644 po/kn/kcm_kwintabbox.po create mode 100644 po/kn/kcmkwincompositing.po create mode 100644 po/kn/kcmkwindecoration.po create mode 100644 po/kn/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ko/kcm_kwintabbox.po create mode 100644 po/ko/kcmkwincompositing.po create mode 100644 po/ko/kcmkwindecoration.po create mode 100644 po/ko/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ku/kcmkwincompositing.po create mode 100644 po/ku/kcmkwindecoration.po create mode 100644 po/ku/kcmkwinrules.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_kwindesktop.po create mode 100644 po/lt/kcm_kwintabbox.po create mode 100644 po/lt/kcmkwincompositing.po create mode 100644 po/lt/kcmkwindecoration.po create mode 100644 po/lt/kcmkwinrules.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/lv/kcm_kwindesktop.po create mode 100644 po/lv/kcm_kwintabbox.po create mode 100644 po/lv/kcmkwincompositing.po create mode 100644 po/lv/kcmkwindecoration.po create mode 100644 po/lv/kcmkwinrules.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_kwindesktop.po create mode 100644 po/mai/kcm_kwintabbox.po create mode 100644 po/mai/kcmkwincompositing.po create mode 100644 po/mai/kcmkwindecoration.po create mode 100644 po/mai/kcmkwinrules.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_kwindesktop.po create mode 100644 po/mk/kcmkwincompositing.po create mode 100644 po/mk/kcmkwindecoration.po create mode 100644 po/mk/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ml/kcmkwincompositing.po create mode 100644 po/ml/kcmkwindecoration.po create mode 100644 po/ml/kcmkwinrules.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/mr/kcm-kwin-scripts.po create mode 100644 po/mr/kcm_kwindesktop.po create mode 100644 po/mr/kcm_kwintabbox.po create mode 100644 po/mr/kcmkwincompositing.po create mode 100644 po/mr/kcmkwindecoration.po create mode 100644 po/mr/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ms/kcm_kwintabbox.po create mode 100644 po/ms/kcmkwincompositing.po create mode 100644 po/ms/kcmkwindecoration.po create mode 100644 po/ms/kcmkwinrules.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_kwindesktop.po create mode 100644 po/nb/kcm_kwintabbox.po create mode 100644 po/nb/kcmkwincompositing.po create mode 100644 po/nb/kcmkwindecoration.po create mode 100644 po/nb/kcmkwinrules.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_kwindesktop.po create mode 100644 po/nds/kcm_kwintabbox.po create mode 100644 po/nds/kcmkwincompositing.po create mode 100644 po/nds/kcmkwindecoration.po create mode 100644 po/nds/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ne/kcmkwincompositing.po create mode 100644 po/ne/kcmkwindecoration.po create mode 100644 po/ne/kcmkwinrules.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_kwindesktop.po create mode 100644 po/nl/kcm_kwintabbox.po create mode 100644 po/nl/kcmkwincompositing.po create mode 100644 po/nl/kcmkwindecoration.po create mode 100644 po/nl/kcmkwinrules.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_kwindesktop.po create mode 100644 po/nn/kcm_kwintabbox.po create mode 100644 po/nn/kcmkwincompositing.po create mode 100644 po/nn/kcmkwindecoration.po create mode 100644 po/nn/kcmkwinrules.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_kwindesktop.po create mode 100644 po/oc/kcmkwincompositing.po create mode 100644 po/oc/kcmkwindecoration.po create mode 100644 po/oc/kcmkwinrules.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/or/kcm_kwindesktop.po create mode 100644 po/or/kcmkwincompositing.po create mode 100644 po/or/kcmkwindecoration.po create mode 100644 po/or/kcmkwinrules.po create mode 100644 po/or/kcmkwm.po create mode 100644 po/or/kwin.po create mode 100644 po/or/kwin_clients.po create mode 100644 po/or/kwin_effects.po create mode 100644 po/pa/kcm-kwin-scripts.po create mode 100644 po/pa/kcm_kwindesktop.po create mode 100644 po/pa/kcm_kwintabbox.po create mode 100644 po/pa/kcmkwincompositing.po create mode 100644 po/pa/kcmkwindecoration.po create mode 100644 po/pa/kcmkwinrules.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_kwindesktop.po create mode 100644 po/pl/kcm_kwintabbox.po create mode 100644 po/pl/kcmkwincompositing.po create mode 100644 po/pl/kcmkwindecoration.po create mode 100644 po/pl/kcmkwinrules.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_kwindesktop.po create mode 100644 po/pt/kcm_kwintabbox.po create mode 100644 po/pt/kcmkwincompositing.po create mode 100644 po/pt/kcmkwindecoration.po create mode 100644 po/pt/kcmkwinrules.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_kwindesktop.po create mode 100644 po/pt_BR/kcm_kwintabbox.po create mode 100644 po/pt_BR/kcmkwincompositing.po create mode 100644 po/pt_BR/kcmkwindecoration.po create mode 100644 po/pt_BR/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ro/kcm_kwintabbox.po create mode 100644 po/ro/kcmkwincompositing.po create mode 100644 po/ro/kcmkwindecoration.po create mode 100644 po/ro/kcmkwinrules.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/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/Face-smile.png 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_kwindesktop.po create mode 100644 po/ru/kcm_kwintabbox.po create mode 100644 po/ru/kcmkwincompositing.po create mode 100644 po/ru/kcmkwindecoration.po create mode 100644 po/ru/kcmkwinrules.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_kwindesktop.po create mode 100644 po/se/kcmkwincompositing.po create mode 100644 po/se/kcmkwindecoration.po create mode 100644 po/se/kcmkwinrules.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_kwindesktop.po create mode 100644 po/si/kcm_kwintabbox.po create mode 100644 po/si/kcmkwincompositing.po create mode 100644 po/si/kcmkwindecoration.po create mode 100644 po/si/kcmkwinrules.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_kwindesktop.po create mode 100644 po/sk/kcm_kwintabbox.po create mode 100644 po/sk/kcmkwincompositing.po create mode 100644 po/sk/kcmkwindecoration.po create mode 100644 po/sk/kcmkwinrules.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_kwindesktop.po create mode 100644 po/sl/kcm_kwintabbox.po create mode 100644 po/sl/kcmkwincompositing.po create mode 100644 po/sl/kcmkwindecoration.po create mode 100644 po/sl/kcmkwinrules.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_kwindesktop.po create mode 100644 po/sq/kcmkwincompositing.po create mode 100644 po/sq/kcmkwindecoration.po create mode 100644 po/sq/kcmkwinrules.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_kwindesktop.po create mode 100644 po/sr/kcm_kwintabbox.po create mode 100644 po/sr/kcmkwincompositing.po create mode 100644 po/sr/kcmkwindecoration.po create mode 100644 po/sr/kcmkwinrules.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_kwindesktop.po create mode 100644 po/sr@ijekavian/kcm_kwintabbox.po create mode 100644 po/sr@ijekavian/kcmkwincompositing.po create mode 100644 po/sr@ijekavian/kcmkwindecoration.po create mode 100644 po/sr@ijekavian/kcmkwinrules.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_kwindesktop.po create mode 100644 po/sr@ijekavianlatin/kcm_kwintabbox.po create mode 100644 po/sr@ijekavianlatin/kcmkwincompositing.po create mode 100644 po/sr@ijekavianlatin/kcmkwindecoration.po create mode 100644 po/sr@ijekavianlatin/kcmkwinrules.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_kwindesktop.po create mode 100644 po/sr@latin/kcm_kwintabbox.po create mode 100644 po/sr@latin/kcmkwincompositing.po create mode 100644 po/sr@latin/kcmkwindecoration.po create mode 100644 po/sr@latin/kcmkwinrules.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/docs/kcontrol/desktop/index.docbook create mode 100644 po/sv/docs/kcontrol/kwindecoration/index.docbook create mode 100644 po/sv/docs/kcontrol/kwineffects/index.docbook create mode 100644 po/sv/docs/kcontrol/kwinscreenedges/index.docbook create mode 100644 po/sv/docs/kcontrol/kwintabbox/index.docbook create mode 100644 po/sv/docs/kcontrol/windowbehaviour/index.docbook create mode 100644 po/sv/docs/kcontrol/windowspecific/index.docbook create mode 100644 po/sv/kcm-kwin-scripts.po create mode 100644 po/sv/kcm_kwindesktop.po create mode 100644 po/sv/kcm_kwintabbox.po create mode 100644 po/sv/kcmkwincompositing.po create mode 100644 po/sv/kcmkwindecoration.po create mode 100644 po/sv/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ta/kcmkwincompositing.po create mode 100644 po/ta/kcmkwindecoration.po create mode 100644 po/ta/kcmkwinrules.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_kwindesktop.po create mode 100644 po/te/kcmkwincompositing.po create mode 100644 po/te/kcmkwindecoration.po create mode 100644 po/te/kcmkwinrules.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_kwindesktop.po create mode 100644 po/tg/kcmkwincompositing.po create mode 100644 po/tg/kcmkwindecoration.po create mode 100644 po/tg/kcmkwinrules.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_kwindesktop.po create mode 100644 po/th/kcm_kwintabbox.po create mode 100644 po/th/kcmkwincompositing.po create mode 100644 po/th/kcmkwindecoration.po create mode 100644 po/th/kcmkwinrules.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_kwindesktop.po create mode 100644 po/tr/kcm_kwintabbox.po create mode 100644 po/tr/kcmkwincompositing.po create mode 100644 po/tr/kcmkwindecoration.po create mode 100644 po/tr/kcmkwinrules.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_kwindesktop.po create mode 100644 po/ug/kcm_kwintabbox.po create mode 100644 po/ug/kcmkwincompositing.po create mode 100644 po/ug/kcmkwindecoration.po create mode 100644 po/ug/kcmkwinrules.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_kwindesktop.po create mode 100644 po/uk/kcm_kwintabbox.po create mode 100644 po/uk/kcmkwincompositing.po create mode 100644 po/uk/kcmkwindecoration.po create mode 100644 po/uk/kcmkwinrules.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/kcmkwindecoration.po create mode 100644 po/uz/kcmkwinrules.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/kcmkwindecoration.po create mode 100644 po/uz@cyrillic/kcmkwinrules.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/wa/kcm_kwindesktop.po create mode 100644 po/wa/kcm_kwintabbox.po create mode 100644 po/wa/kcmkwincompositing.po create mode 100644 po/wa/kcmkwindecoration.po create mode 100644 po/wa/kcmkwinrules.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/kcmkwindecoration.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_kwindesktop.po create mode 100644 po/zh_CN/kcm_kwintabbox.po create mode 100644 po/zh_CN/kcmkwincompositing.po create mode 100644 po/zh_CN/kcmkwindecoration.po create mode 100644 po/zh_CN/kcmkwinrules.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_kwindesktop.po create mode 100644 po/zh_TW/kcm_kwintabbox.po create mode 100644 po/zh_TW/kcmkwincompositing.po create mode 100644 po/zh_TW/kcmkwindecoration.po create mode 100644 po/zh_TW/kcmkwinrules.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 qml/virtualkeyboard/main.qml create mode 100644 resources.qrc create mode 100644 rootinfo_filter.cpp create mode 100644 rootinfo_filter.h create mode 100644 rules.cpp create mode 100644 rules.h create mode 100644 sc-apps-kwin.svgz create mode 100644 scene.cpp create mode 100644 scene.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/enforcedeco/contents/code/main.js create mode 100644 scripts/enforcedeco/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 settings.kcfgc create mode 100644 shaders/1.10/lanczos-fragment.glsl create mode 100644 shaders/1.40/lanczos-fragment.glsl create mode 100644 shadow.cpp create mode 100644 shadow.h create mode 100644 shell_client.cpp create mode 100644 shell_client.h create mode 100644 shortcutdialog.ui create mode 100644 sm.cpp create mode 100644 sm.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 tabgroup.cpp create mode 100644 tabgroup.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/orientationtest.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/waylandclienttest.cpp create mode 100644 tests/waylandclienttest.h 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_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 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_cursor_theme.cpp create mode 100644 wayland_cursor_theme.h create mode 100644 wayland_server.cpp create mode 100644 wayland_server.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 x11eventfilter.cpp create mode 100644 x11eventfilter.h create mode 100644 xcbutils.cpp create mode 100644 xcbutils.h create mode 100644 xkb.cpp create mode 100644 xkb.h create mode 100644 xkb_qt_mapping.h diff --git a/.arcconfig b/.arcconfig new file mode 100644 index 0000000..bc0df43 --- /dev/null +++ b/.arcconfig @@ -0,0 +1,4 @@ +{ + "phabricator.uri" : "https://phabricator.kde.org/" +} + diff --git a/16-apps-kwin.png b/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/48-apps-kwin.png b/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/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b598a2c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,725 @@ +cmake_minimum_required(VERSION 3.1 FATAL_ERROR) + +project(KWIN) +set(PROJECT_VERSION "5.14.5") +set(PROJECT_VERSION_MAJOR 5) + +set(QT_MIN_VERSION "5.11.0") +set(KF5_MIN_VERSION "5.42.0") + +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} ${ECM_KDE_MODULE_DIR}) + +find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS + Concurrent + Core + DBus + Quick + QuickWidgets + Sensors + Script + 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(ECMInstallIcons) +include(ECMOptionalAddSubdirectory) + +add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0 -DQT_USE_QSTRINGBUILDER) + +set(CMAKE_CXX_STANDARD 14) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +if("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-inconsistent-missing-override") +endif() + +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 + Init + Notifications + Package + Plasma + WidgetsAddons + WindowSystem + IconThemes + IdleTime + Wayland +) +# required frameworks by config modules +find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS + Completion + Declarative + KCMUtils + KIO + TextWidgets + NewStuff + Service + 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(KDecoration2 5.13.0 CONFIG REQUIRED) + +find_package(KScreenLocker CONFIG REQUIRED) +set_package_properties(KScreenLocker PROPERTIES + TYPE REQUIRED + PURPOSE "For screenlocker integration in kwin_wayland") + +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 "http://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 REQUIRED COMPONENTS Cursor 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 and QPA with EGL support.") +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 "http://www.freedesktop.org/software/systemd/libudev/" + 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() + +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 "http://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 + XCB + XFIXES + DAMAGE + COMPOSITE + SHAPE + SYNC + RENDER + RANDR + KEYSYMS + IMAGE + SHM + GLX + CURSOR + ICCCM +) +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 "http://www.freetype.org" + TYPE REQUIRED + PURPOSE "Needed for KWin's QPA plugin." + ) +find_package(Fontconfig REQUIRED) +set_package_properties(Fontconfig PROPERTIES DESCRIPTION "Font access configuration library" + URL "http://www.freedesktop.org/wiki/Software/fontconfig" + TYPE REQUIRED + PURPOSE "Needed for KWin's QPA plugin." + ) + +find_package(Xwayland) +set_package_properties(Xwayland PROPERTIES + URL "http://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}) + +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") +set(KWIN_SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}) + +# 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) +configure_file(config-kwin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kwin.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") + +########### global ############### +set(kwin_effects_dbus_xml ${CMAKE_CURRENT_SOURCE_DIR}/org.kde.kwin.Effects.xml) + +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_KDEINIT_SRCS + workspace.cpp + dbusinterface.cpp + abstract_client.cpp + client.cpp + client_machine.cpp + cursor.cpp + debug_console.cpp + tabgroup.cpp + focuschain.cpp + globalshortcuts.cpp + input.cpp + input_event.cpp + input_event_spy.cpp + keyboard_input.cpp + keyboard_layout.cpp + keyboard_layout_switching.cpp + keyboard_repeat.cpp + pointer_input.cpp + touch_input.cpp + netinfo.cpp + placement.cpp + atoms.cpp + utils.cpp + layers.cpp + main.cpp + options.cpp + outline.cpp + events.cpp + killwindow.cpp + geometrytip.cpp + screens.cpp + outputscreens.cpp + shadow.cpp + sm.cpp + group.cpp + manage.cpp + overlaywindow.cpp + activation.cpp + useractions.cpp + geometry.cpp + rules.cpp + composite.cpp + toplevel.cpp + unmanaged.cpp + scene.cpp + screenlockerwatcher.cpp + thumbnailitem.cpp + lanczosfilter.cpp + deleted.cpp + effects.cpp + effectloader.cpp + virtualdesktops.cpp + xcbutils.cpp + x11eventfilter.cpp + logind.cpp + onscreennotification.cpp + osd.cpp + screenedge.cpp + scripting/scripting.cpp + scripting/workspace_wrapper.cpp + scripting/meta.cpp + scripting/scriptedeffect.cpp + scripting/scriptingutils.cpp + scripting/timer.cpp + scripting/scripting_model.cpp + scripting/dbuscall.cpp + scripting/screenedgeitem.cpp + scripting/scripting_logging.cpp + decorations/decoratedclient.cpp + decorations/decorationbridge.cpp + decorations/decorationpalette.cpp + decorations/settings.cpp + decorations/decorationrenderer.cpp + decorations/decorations_logging.cpp + platform.cpp + abstract_output.cpp + shell_client.cpp + wayland_server.cpp + wayland_cursor_theme.cpp + virtualkeyboard.cpp + virtualkeyboard_dbus.cpp + appmenu.cpp + modifier_only_shortcuts.cpp + xkb.cpp + gestures.cpp + popup_input_filter.cpp + colorcorrection/manager.cpp + colorcorrection/colorcorrectdbusinterface.cpp + colorcorrection/suncalc.cpp + abstract_opengl_context_attribute_builder.cpp + egl_context_attribute_builder.cpp + was_user_interaction_x11_filter.cpp + moving_client_x11_filter.cpp + window_property_notify_x11_filter.cpp + rootinfo_filter.cpp + orientation_sensor.cpp + idle_inhibition.cpp + libinput/context.cpp + libinput/connection.cpp + libinput/device.cpp + libinput/events.cpp + libinput/libinput_logging.cpp + udev.cpp + ) + +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category(kwin_KDEINIT_SRCS + HEADER + colorcorrect_logging.h + IDENTIFIER + KWIN_COLORCORRECTION + CATEGORY_NAME + kwin_colorcorrection + DEFAULT_SEVERITY + Critical +) + +if(KWIN_BUILD_TABBOX) + set( + kwin_KDEINIT_SRCS ${kwin_KDEINIT_SRCS} + tabbox/tabbox.cpp + tabbox/clientmodel.cpp + tabbox/desktopchain.cpp + tabbox/desktopmodel.cpp + tabbox/switcheritem.cpp + tabbox/tabboxconfig.cpp + tabbox/tabboxhandler.cpp + tabbox/tabbox_logging.cpp + tabbox/x11_filter.cpp + ) +endif() + +if(KWIN_BUILD_ACTIVITIES) + set( + kwin_KDEINIT_SRCS ${kwin_KDEINIT_SRCS} + activities.cpp + ) +endif() + +if (HAVE_LINUX_VT_H) + set(kwin_KDEINIT_SRCS + ${kwin_KDEINIT_SRCS} + virtual_terminal.cpp + ) +endif() + +kconfig_add_kcfg_files(kwin_KDEINIT_SRCS settings.kcfgc) +kconfig_add_kcfg_files(kwin_KDEINIT_SRCS colorcorrection/colorcorrect_settings.kcfgc) + +qt5_add_dbus_adaptor( kwin_KDEINIT_SRCS org.kde.KWin.xml dbusinterface.h KWin::DBusInterface ) +qt5_add_dbus_adaptor( kwin_KDEINIT_SRCS org.kde.kwin.Compositing.xml dbusinterface.h KWin::CompositorDBusInterface ) +qt5_add_dbus_adaptor( kwin_KDEINIT_SRCS org.kde.kwin.ColorCorrect.xml colorcorrection/colorcorrectdbusinterface.h KWin::ColorCorrect::ColorCorrectDBusInterface ) +qt5_add_dbus_adaptor( kwin_KDEINIT_SRCS ${kwin_effects_dbus_xml} effects.h KWin::EffectsHandlerImpl ) +qt5_add_dbus_adaptor( kwin_KDEINIT_SRCS org.kde.kwin.OrientationSensor.xml orientation_sensor.h KWin::OrientationSensor) + +qt5_add_dbus_interface( kwin_KDEINIT_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/org.freedesktop.ScreenSaver.xml screenlocker_interface) + +qt5_add_dbus_interface( kwin_KDEINIT_SRCS org.kde.kappmenu.xml appmenu_interface ) + +qt5_add_resources( kwin_KDEINIT_SRCS resources.qrc ) + +ki18n_wrap_ui(kwin_KDEINIT_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::Sensors + Qt5::Script +) + +set(kwin_KDE_LIBS + KF5::ConfigCore + KF5::CoreAddons + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::GlobalAccelPrivate + KF5::I18n + KF5::Notifications + KF5::Package + KF5::Plasma + KF5::WindowSystem + KF5::QuickAddons + KDecoration2::KDecoration + KDecoration2::KDecoration2Private + PW::KScreenLocker +) + +set(kwin_XLIB_LIBS + ${X11_X11_LIB} + ${X11_ICE_LIB} + ${X11_SM_LIB} +) + +set(kwin_XCB_LIBS + XCB::XCB + XCB::XFIXES + XCB::DAMAGE + XCB::COMPOSITE + XCB::SHAPE + XCB::SYNC + XCB::RENDER + XCB::RANDR + XCB::KEYSYMS + XCB::SHM + XCB::GLX + XCB::ICCCM +) + +set(kwin_WAYLAND_LIBS + XKB::XKB + KF5::WaylandClient + KF5::WaylandServer + Wayland::Cursor + ${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_KDEINIT_SRCS}) + +set_target_properties(kwin PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} + ) + +target_link_libraries(kwin ${kwinLibs}) +generate_export_header(kwin EXPORT_FILE_NAME kwin_export.h) + +target_link_libraries(kwin kwinglutils ${epoxy_LIBRARY}) + +kf5_add_kdeinit_executable(kwin_x11 main_x11.cpp) +target_link_libraries(kdeinit_kwin_x11 kwin KF5::Crash Qt5::X11Extras) + +install(TARGETS kwin ${INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP ) +install(TARGETS kdeinit_kwin_x11 ${INSTALL_TARGETS_DEFAULT_ARGS} ) +install(TARGETS kwin_x11 ${INSTALL_TARGETS_DEFAULT_ARGS} ) + +add_executable(kwin_wayland tabletmodemanager.cpp main_wayland.cpp) +target_link_libraries(kwin_wayland kwin) +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.xml + org.kde.kwin.Compositing.xml + org.kde.kwin.ColorCorrect.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} ) + +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 +) + +add_subdirectory(qml) +add_subdirectory(packageplugins) + +if (BUILD_TESTING) + add_subdirectory(autotests) + add_subdirectory(tests) +endif() + +if (KF5DocTools_FOUND) + add_subdirectory(doc) +endif() + +add_subdirectory(kconf_update) + +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}) + +find_package(KF5I18n CONFIG REQUIRED) +ki18n_install(po) + + find_package(KF5DocTools CONFIG) + if(KF5DocTools_FOUND) + kdoctools_install(po) + endif() diff --git a/COMPLIANCE b/COMPLIANCE new file mode 100644 index 0000000..493ec00 --- /dev/null +++ b/COMPLIANCE @@ -0,0 +1,254 @@ +W A R N I N G: +-------------- +This document is a work in progress and is in no way complete or accurate! +Its current purpose is in aiding the KWin NetWM audit for a future KWin release. + +NetWM Compliance Document: +========================== + +Listed below are all the NetWM (or EWM) hints decided upon on freedesktop.org +(as of version 1.3draft, Nov 27, 2002) and KWin's current level of +compliance with the spec. Some parts also involve the pager and clients which +this document will cater for as well where applicable. + +If you modify the level of NetWM compliance (via modification of kwin/*, +kdecore/netwm.* or kdecore/kwin.* etc.), or notice any new hints that +were added after version 1.2, please modify this document appropriately. +Properties are ordered in the table in the order they are found in the +specification. To list any important notes regarding a property, just +add them as follows: + +_NET_NUMBER_OF_DESKTOPS root window property done + +----------------------------------------------------------------+ + | This property SHOULD be updated by the Window Manager to | + | indicate the number of virtual desktops. KWin DOES update this | + | property when the pager changes the number of desktops. | + +----------------------------------------------------------------+ + +If you have any questions regarding the specification, feel free to ask on the KWin +mailing list , or on the Window Manager Spec list . + -- Karol + +( + compliance : + - = none, + / = partial, + + = complete, + * = KWin is compliant, but something else needs checking (e.g. Qt) + ? = unknown +) + + +NETWM spec compliance (whole document): +version 1.2 +====================== + ++ 1. ++ 2.3. Feature not implemented. ++ 2.4. Feature not implemented. ++ 2.5. ++ 2. (rest of the section) ++ 3.1. + This property is complete in the sense that all implemented properties + are listed here. + CHECKME : check it's complete +/ 3.2. + The spec requires that _NET_CLIENT_LIST contains the windows in their + initial mapping order, which is currently not true for NET::Desktop + windows. + Note that xprop lists only first element in WINDOW type properties. ++ 3.3. + Note that KWin does not use the virtual root windows technique, + so it doesn't set _NET_VIRTUAL_ROOTS at all. ++ 3.4. + KWin doesn't implement large desktops, so it ignores + the message, and only sets the property to the screen size. ++ 3.5. + KWin doesn't implement viewports, so it correctly sets + the property to (0,0) pairs and ignores the message. ++ 3.6. ++ 3.7. ++ 3.8. + KWin currently extends the message a bit, with data.l[0] being 1 or 2, + meaning 'from application'/'from pager', and data.l[1] contains + timestamp. This is used for focus stealing prevention purposes, and + will be proposed for next version of the spec. ++ 3.9. ++ 3.10. ++ 3.11. + KWin doesn't use the virtual roots technique for creating virtual + desktops, so it doesn't set the property. +- 3.12. +- 3.13. ++ 4.1. ++ 4.2. ++ 4.3. + Due to implementation details KWin actually allows moving or resizing + by keyboard when requested to be done by mouse, and vice versa. ++ 5.1. ++ 5.2. ++ 5.3. ++ 5.4. ++ 5.5. +/ 5.6. The handling of _NET_WM_WINDOW_TYPE itself is correct and complete. + Supported window types: DESKTOP, DOCK, TOOLBAR, MENU, UTILITY, + SPLASH, DIALOG, NORMAL. + UTILITY should get better placement. + TOOLBAR - many parts in KDE still treat this as "tool" window. + - should the decoration be shown for the toolbars? + KDE extensions: + _KDE_NET_WM_WINDOW_TYPE_OVERRIDE - this seems to mean "this window + should be borderless", but it's actually used also for other + things, like fullscreen windows. The plan is to get rid of this + flawed thing as soon as possible. +/ 5.7. + The handling of _NET_WM_STATE itself is correct and complete. + Supported states: MODAL, MAXIMIZED_VERT, MAXIMIZED_HORZ, SHADED, + SKIP_TASKBAR, SKIP_PAGER, HIDDEN, ABOVE, BELOW. + STICKY is not supported, because KWin doesn't implement viewports. + BELOW - in order to make 'allow windows to cover the panel' feature + in Kicker work KWin interprets this state a bit differently + for DOCK windows. While normal DOCK windows are in their + extra layer above normal windows, making them BELOW doesn't + move them below normal windows, but only to the same layer, so + that windows can hide Kicker, but Kicker can be also raised + above the windows. A bit hacky, but it's not really against + the spec, and I have no better idea. + KDE extensions: + _NET_WM_STATE_STAYS_ON_TOP - has the same meaning like ABOVE, + and is deprecated in favour of it; it lacks the _KDE prefix +* 5.8. + The handling of _NET_WM_ALLOWED_ACTIONS itself is correct and complete. + Supported actions: MOVE, RESIZE, MINIMIZE, SHADE, MAXIMIZE_HORZ, + MAXIMIZE_VERT, CHANGE_DESKTOP, CLOSE + STICK is not supported, because KWin does not implement viewports. + Kicker etc. need to be updated. ++ 5.9. +* 5.10. + Property is not used in KWin. + Kicker needs to be checked. +* 5.11. + KWin's handling of the property is correct. + Qt should be checked. ++ 5.12. +- 5.13. + Property is not used in KWin, KWin never provides icons for iconified windows. + Kicker or its taskbar don't set it either. However, the property is flawed, + and should be replaced by manager selection or similar mechanism. ++ 6.1. ++ 6. (rest) ++ 7.4. + The urgency hint is mapped to the _NET_WM_DEMANDS_ATTENTION flag. +* 7.5. + Qt often sets maximum size smaller than minimum size. This seems to be caused + by delayed layout calculations. +* 7.6. + Kicker should be checked. +? 7.7. ++ 7. (rest of the section) + + ++ _NET_WM_FULLSCREEN_MONITORS Status: Done. + +----------------------------------------------------------------+ + | The Window Manager MUST keep this list updated to reflect the | + | current state of the window. The application window sends this | + | in a ClientMessage to the root window. KWin persists this info | + | both internally as well as against the application window. | + | This data is used to spread the fullscreen application window | + | across the requested topology, if valid. | + +----------------------------------------------------------------+ + +ICCCM spec compliance (whole document): +version 2.0 +====================== + +/ 1.2.3. + KWin uses KWIN_RUNNING atom that's missing the leading underscore. + Some parts of KDE perhaps may be missing the leading underscore. +/ 1.2.6. + Should be checked. ++ 1. (rest of the section) ++ 2.8. kmanagerselection.* in kdecore ++ 2. (rest of the section) + Not a KWin thing. +* - patch sent to TT to make QClipboard sufficiently compliant ++ 3. + Feature not supported, obsolete. ++ 4.1.1 ++ 4.1.2 (intro) ++ 4.1.2.1 + Used as a fallback for _NET_WM_NAME. ++ 4.1.2.2 + Used as a fallback for _NET_WM_ICON_NAME. +? 4.1.2.3 +? - PSize, PPosition, USize, UPosition +? - clients - Qt simply sets both ++ - PWinGravity - window geometry constraints have higher priority than gravity +/ - PBaseSize - PMinSize is not used as a fallback for size increments ++ - (the rest) +/ 4.1.2.4 ++ - input - see 4.1.7 ++ - initial_state ++ - icon - feature not supported ++ - window_group ++ - urgency - mapped to _NET_WM_DEMANDS_ATTENTION +/ 4.1.2.5 - it should be checked it's used correctly in Kicker etc. +/ 4.1.2.6 - should be checked + NETWM section 7.3. is supported too, even though it's a slight ICCCM violation. ++ 4.1.2.7 +- 4.1.2.8 + See 4.1.8. ++ 4.1.2.9 - handled by Xlib call ++ 4.1.3.1 ++ 4.1.3.2 + Feature not supported (4.1.2.4 - icons) +* 4.1.4 it should be checked Qt/KDE clients handle this properly +/ 4.1.5 + This needs fixing. ++ 4.1.6 ++ 4.1.7 +- 4.1.8 + KWin only installs colormap required by the active window. +- 4.1.9 + Feature not supported, except for WM_ICON_NAME as a fallback for _NET_WM_ICON_NAME. ++ 4.1.10 ++ 4.1.11 + Window groups are only used for supporting NETWM section 7.3. ++ 4.2.5 +/ 4.2.7 + Qt doesn't set revert-to to Parent. ++ 4.2.8.1 frozen clients may be XKill-ed upon a user request though ++ 4.3 +? 4.4 ++ 4. (rest of the section) ++ 5.3. not KWin related ++ 5. (rest of the section ) +? 6.1. clients thing +? 6.2. clients thing - Qt perhaps should force rule 2. ++ 6.3. +? 6. (rest of the section) ++ 7. - no idea what it is, but it doesn't seem to be KWin related ++ 8. + + +KDE-specific extensions (for completeness): + +Property Name Type +========================================================================== +_KDE_WM_CHANGE_STATE root window message +_KDE_NET_SYSTEM_TRAY_WINDOWS root window property +_KDE_NET_WM_SYSTEM_TRAY_WINDOW_FOR window property +_KDE_NET_WM_FRAME_STRUT window property +_NET_WM_CONTEXT_HELP + - Qt extension + - has no vendor prefix even though it's not part of the spec +_NET_WM_STATE_STAYS_ON_TOP + - KDE extension + - has no vendor prefix even though it's not part of the spec + - deprecated in favor of _NET_WM_STATE_KEEP_ABOVE +_KDE_NET_WM_WINDOW_TYPE_OVERRIDE + - window type, makes the window borderless + - unclear semantics, used also for fullscreen windows + - deprecated in favor of other window types + +========================================================================== diff --git a/CONFIGURING b/CONFIGURING new file mode 100644 index 0000000..4a27741 --- /dev/null +++ b/CONFIGURING @@ -0,0 +1,73 @@ +CONTENTS: +========= + + +1. Pre-configuring window-specific settings + + + + + +1. Pre-configuring window-specific settings +=========================================== + +Window-specific settings is a feature of KWin that allows specifying some +settings only for a specific window or windows. See the Window-specific +settings section in the KWin configuration and the Special settings +menu entries in Alt+F3/Advanced menu. + +One aspect of window-specific settings is the ability to specify various +workarounds for (usually broken) applications that otherwise don't work +properly with KWin. This section describes how to create additional +window-specific settings that will be automatically used by all users +without any need of manual configuration. + +Example case: + +Application FooBar does not specify any maximum size for its main window, +but when resized to larger size than 1600x1200 it crashes because of a bug. +Manual configuration of a window-specific setting that avoids this problem +is opening and activating this window, selecting +Alt+F3/Advanced/Special window settings, activating tab Workarounds, enabling +setting Maximum size, changing it to Force and entering "1600,1200" as +the maximum size, which will make KWin force this size as the maximum size. + +To create such window-specific setting automatically without a need of doing +it manually for every user (for example when doing a large deployment), follow +these steps: + +- Back up your $KDEHOME/share/config/kwinrulesrc ($KDEHOME usually being $HOME/.kde) + and remove it +- Run 'dcop kwin default reconfigure' +- Create manually all window-specific settings that should be included (see above) +- When done, check in Window-specific settings configuration module + (Alt+F3/Configure window behavior/Window-specific settings) that all rules are + included +- Create a copy of $KDEHOME/share/config/kwinrulesrc and restore the original one +- Rename the copy (i.e. the newly created kwinrulesrc) to have its unique name + (e.g. foobar_fix_maxsize in this example case) +- Be careful with manual modifications of the file, especially make sure the count= + field in the [General] group is updated if needed +- Create a file for kconfig_update like this (named kwin_foobar_fix_maxsize.upd + in this example): + +# kwin_foobar_fix_maxsize.upd start # +Id=foobar_fix_maxsize +File=kwinrules_update +Group=Dummy +Options=overwrite +ScriptArguments=foobar_fix_maxsize +Script=kwin_update_default_rules + +# kwin_foobar_fix_maxsize.upd end # + +- The kconfig_file (kwin_foobar_fix_maxsize.upd) is to be placed + in $KDEDIR/share/apps/kconf_update/ +- The file with the window-specific settings (foobar_fix_maxsize) is to be placed + in $KDEDIR/share/apps/kwin/default_rules/ + + +All KDE user accounts should have these new window-specific settings added +automatically during next KDE startup (or within few seconds if they are active). +They can be checked again in the Window-specific settings configuration module of KWin. + diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..5185fd3 --- /dev/null +++ b/COPYING @@ -0,0 +1,346 @@ +NOTE! The GPL below is copyrighted by the Free Software Foundation, but +the instance of code that it refers to (the kde programs) are copyrighted +by the authors who actually wrote it. + +--------------------------------------------------------------------------- + + 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 Library 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. + + GNU GENERAL PUBLIC LICENSE + 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) 19yy + + 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) 19yy 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 Library General +Public License instead of this License. diff --git a/COPYING.DOC b/COPYING.DOC new file mode 100644 index 0000000..4a0fe1c --- /dev/null +++ b/COPYING.DOC @@ -0,0 +1,397 @@ + GNU Free Documentation License + Version 1.2, November 2002 + + + Copyright (C) 2000,2001,2002 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. + + +0. PREAMBLE + +The purpose of this License is to make a manual, textbook, or other +functional and useful document "free" in the sense of freedom: to +assure everyone the effective freedom to copy and redistribute it, +with or without modifying it, either commercially or noncommercially. +Secondarily, this License preserves for the author and publisher a way +to get credit for their work, while not being considered responsible +for modifications made by others. + +This License is a kind of "copyleft", which means that derivative +works of the document must themselves be free in the same sense. It +complements the GNU General Public License, which is a copyleft +license designed for free software. + +We have designed this License in order to use it for manuals for free +software, because free software needs free documentation: a free +program should come with manuals providing the same freedoms that the +software does. But this License is not limited to software manuals; +it can be used for any textual work, regardless of subject matter or +whether it is published as a printed book. We recommend this License +principally for works whose purpose is instruction or reference. + + +1. APPLICABILITY AND DEFINITIONS + +This License applies to any manual or other work, in any medium, that +contains a notice placed by the copyright holder saying it can be +distributed under the terms of this License. Such a notice grants a +world-wide, royalty-free license, unlimited in duration, to use that +work under the conditions stated herein. The "Document", below, +refers to any such manual or work. Any member of the public is a +licensee, and is addressed as "you". You accept the license if you +copy, modify or distribute the work in a way requiring permission +under copyright law. + +A "Modified Version" of the Document means any work containing the +Document or a portion of it, either copied verbatim, or with +modifications and/or translated into another language. + +A "Secondary Section" is a named appendix or a front-matter section of +the Document that deals exclusively with the relationship of the +publishers or authors of the Document to the Document's overall subject +(or to related matters) and contains nothing that could fall directly +within that overall subject. (Thus, if the Document is in part a +textbook of mathematics, a Secondary Section may not explain any +mathematics.) The relationship could be a matter of historical +connection with the subject or with related matters, or of legal, +commercial, philosophical, ethical or political position regarding +them. + +The "Invariant Sections" are certain Secondary Sections whose titles +are designated, as being those of Invariant Sections, in the notice +that says that the Document is released under this License. If a +section does not fit the above definition of Secondary then it is not +allowed to be designated as Invariant. The Document may contain zero +Invariant Sections. If the Document does not identify any Invariant +Sections then there are none. + +The "Cover Texts" are certain short passages of text that are listed, +as Front-Cover Texts or Back-Cover Texts, in the notice that says that +the Document is released under this License. A Front-Cover Text may +be at most 5 words, and a Back-Cover Text may be at most 25 words. + +A "Transparent" copy of the Document means a machine-readable copy, +represented in a format whose specification is available to the +general public, that is suitable for revising the document +straightforwardly with generic text editors or (for images composed of +pixels) generic paint programs or (for drawings) some widely available +drawing editor, and that is suitable for input to text formatters or +for automatic translation to a variety of formats suitable for input +to text formatters. A copy made in an otherwise Transparent file +format whose markup, or absence of markup, has been arranged to thwart +or discourage subsequent modification by readers is not Transparent. +An image format is not Transparent if used for any substantial amount +of text. A copy that is not "Transparent" is called "Opaque". + +Examples of suitable formats for Transparent copies include plain +ASCII without markup, Texinfo input format, LaTeX input format, SGML +or XML using a publicly available DTD, and standard-conforming simple +HTML, PostScript or PDF designed for human modification. Examples of +transparent image formats include PNG, XCF and JPG. Opaque formats +include proprietary formats that can be read and edited only by +proprietary word processors, SGML or XML for which the DTD and/or +processing tools are not generally available, and the +machine-generated HTML, PostScript or PDF produced by some word +processors for output purposes only. + +The "Title Page" means, for a printed book, the title page itself, +plus such following pages as are needed to hold, legibly, the material +this License requires to appear in the title page. For works in +formats which do not have any title page as such, "Title Page" means +the text near the most prominent appearance of the work's title, +preceding the beginning of the body of the text. + +A section "Entitled XYZ" means a named subunit of the Document whose +title either is precisely XYZ or contains XYZ in parentheses following +text that translates XYZ in another language. (Here XYZ stands for a +specific section name mentioned below, such as "Acknowledgements", +"Dedications", "Endorsements", or "History".) To "Preserve the Title" +of such a section when you modify the Document means that it remains a +section "Entitled XYZ" according to this definition. + +The Document may include Warranty Disclaimers next to the notice which +states that this License applies to the Document. These Warranty +Disclaimers are considered to be included by reference in this +License, but only as regards disclaiming warranties: any other +implication that these Warranty Disclaimers may have is void and has +no effect on the meaning of this License. + + +2. VERBATIM COPYING + +You may copy and distribute the Document in any medium, either +commercially or noncommercially, provided that this License, the +copyright notices, and the license notice saying this License applies +to the Document are reproduced in all copies, and that you add no other +conditions whatsoever to those of this License. You may not use +technical measures to obstruct or control the reading or further +copying of the copies you make or distribute. However, you may accept +compensation in exchange for copies. If you distribute a large enough +number of copies you must also follow the conditions in section 3. + +You may also lend copies, under the same conditions stated above, and +you may publicly display copies. + + +3. COPYING IN QUANTITY + +If you publish printed copies (or copies in media that commonly have +printed covers) of the Document, numbering more than 100, and the +Document's license notice requires Cover Texts, you must enclose the +copies in covers that carry, clearly and legibly, all these Cover +Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on +the back cover. Both covers must also clearly and legibly identify +you as the publisher of these copies. The front cover must present +the full title with all words of the title equally prominent and +visible. You may add other material on the covers in addition. +Copying with changes limited to the covers, as long as they preserve +the title of the Document and satisfy these conditions, can be treated +as verbatim copying in other respects. + +If the required texts for either cover are too voluminous to fit +legibly, you should put the first ones listed (as many as fit +reasonably) on the actual cover, and continue the rest onto adjacent +pages. + +If you publish or distribute Opaque copies of the Document numbering +more than 100, you must either include a machine-readable Transparent +copy along with each Opaque copy, or state in or with each Opaque copy +a computer-network location from which the general network-using +public has access to download using public-standard network protocols +a complete Transparent copy of the Document, free of added material. +If you use the latter option, you must take reasonably prudent steps, +when you begin distribution of Opaque copies in quantity, to ensure +that this Transparent copy will remain thus accessible at the stated +location until at least one year after the last time you distribute an +Opaque copy (directly or through your agents or retailers) of that +edition to the public. + +It is requested, but not required, that you contact the authors of the +Document well before redistributing any large number of copies, to give +them a chance to provide you with an updated version of the Document. + + +4. MODIFICATIONS + +You may copy and distribute a Modified Version of the Document under +the conditions of sections 2 and 3 above, provided that you release +the Modified Version under precisely this License, with the Modified +Version filling the role of the Document, thus licensing distribution +and modification of the Modified Version to whoever possesses a copy +of it. In addition, you must do these things in the Modified Version: + +A. Use in the Title Page (and on the covers, if any) a title distinct + from that of the Document, and from those of previous versions + (which should, if there were any, be listed in the History section + of the Document). You may use the same title as a previous version + if the original publisher of that version gives permission. +B. List on the Title Page, as authors, one or more persons or entities + responsible for authorship of the modifications in the Modified + Version, together with at least five of the principal authors of the + Document (all of its principal authors, if it has fewer than five), + unless they release you from this requirement. +C. State on the Title page the name of the publisher of the + Modified Version, as the publisher. +D. Preserve all the copyright notices of the Document. +E. Add an appropriate copyright notice for your modifications + adjacent to the other copyright notices. +F. Include, immediately after the copyright notices, a license notice + giving the public permission to use the Modified Version under the + terms of this License, in the form shown in the Addendum below. +G. Preserve in that license notice the full lists of Invariant Sections + and required Cover Texts given in the Document's license notice. +H. Include an unaltered copy of this License. +I. Preserve the section Entitled "History", Preserve its Title, and add + to it an item stating at least the title, year, new authors, and + publisher of the Modified Version as given on the Title Page. If + there is no section Entitled "History" in the Document, create one + stating the title, year, authors, and publisher of the Document as + given on its Title Page, then add an item describing the Modified + Version as stated in the previous sentence. +J. Preserve the network location, if any, given in the Document for + public access to a Transparent copy of the Document, and likewise + the network locations given in the Document for previous versions + it was based on. These may be placed in the "History" section. + You may omit a network location for a work that was published at + least four years before the Document itself, or if the original + publisher of the version it refers to gives permission. +K. For any section Entitled "Acknowledgements" or "Dedications", + Preserve the Title of the section, and preserve in the section all + the substance and tone of each of the contributor acknowledgements + and/or dedications given therein. +L. Preserve all the Invariant Sections of the Document, + unaltered in their text and in their titles. Section numbers + or the equivalent are not considered part of the section titles. +M. Delete any section Entitled "Endorsements". Such a section + may not be included in the Modified Version. +N. Do not retitle any existing section to be Entitled "Endorsements" + or to conflict in title with any Invariant Section. +O. Preserve any Warranty Disclaimers. + +If the Modified Version includes new front-matter sections or +appendices that qualify as Secondary Sections and contain no material +copied from the Document, you may at your option designate some or all +of these sections as invariant. To do this, add their titles to the +list of Invariant Sections in the Modified Version's license notice. +These titles must be distinct from any other section titles. + +You may add a section Entitled "Endorsements", provided it contains +nothing but endorsements of your Modified Version by various +parties--for example, statements of peer review or that the text has +been approved by an organization as the authoritative definition of a +standard. + +You may add a passage of up to five words as a Front-Cover Text, and a +passage of up to 25 words as a Back-Cover Text, to the end of the list +of Cover Texts in the Modified Version. Only one passage of +Front-Cover Text and one of Back-Cover Text may be added by (or +through arrangements made by) any one entity. If the Document already +includes a cover text for the same cover, previously added by you or +by arrangement made by the same entity you are acting on behalf of, +you may not add another; but you may replace the old one, on explicit +permission from the previous publisher that added the old one. + +The author(s) and publisher(s) of the Document do not by this License +give permission to use their names for publicity for or to assert or +imply endorsement of any Modified Version. + + +5. COMBINING DOCUMENTS + +You may combine the Document with other documents released under this +License, under the terms defined in section 4 above for modified +versions, provided that you include in the combination all of the +Invariant Sections of all of the original documents, unmodified, and +list them all as Invariant Sections of your combined work in its +license notice, and that you preserve all their Warranty Disclaimers. + +The combined work need only contain one copy of this License, and +multiple identical Invariant Sections may be replaced with a single +copy. If there are multiple Invariant Sections with the same name but +different contents, make the title of each such section unique by +adding at the end of it, in parentheses, the name of the original +author or publisher of that section if known, or else a unique number. +Make the same adjustment to the section titles in the list of +Invariant Sections in the license notice of the combined work. + +In the combination, you must combine any sections Entitled "History" +in the various original documents, forming one section Entitled +"History"; likewise combine any sections Entitled "Acknowledgements", +and any sections Entitled "Dedications". You must delete all sections +Entitled "Endorsements". + + +6. COLLECTIONS OF DOCUMENTS + +You may make a collection consisting of the Document and other documents +released under this License, and replace the individual copies of this +License in the various documents with a single copy that is included in +the collection, provided that you follow the rules of this License for +verbatim copying of each of the documents in all other respects. + +You may extract a single document from such a collection, and distribute +it individually under this License, provided you insert a copy of this +License into the extracted document, and follow this License in all +other respects regarding verbatim copying of that document. + + +7. AGGREGATION WITH INDEPENDENT WORKS + +A compilation of the Document or its derivatives with other separate +and independent documents or works, in or on a volume of a storage or +distribution medium, is called an "aggregate" if the copyright +resulting from the compilation is not used to limit the legal rights +of the compilation's users beyond what the individual works permit. +When the Document is included in an aggregate, this License does not +apply to the other works in the aggregate which are not themselves +derivative works of the Document. + +If the Cover Text requirement of section 3 is applicable to these +copies of the Document, then if the Document is less than one half of +the entire aggregate, the Document's Cover Texts may be placed on +covers that bracket the Document within the aggregate, or the +electronic equivalent of covers if the Document is in electronic form. +Otherwise they must appear on printed covers that bracket the whole +aggregate. + + +8. TRANSLATION + +Translation is considered a kind of modification, so you may +distribute translations of the Document under the terms of section 4. +Replacing Invariant Sections with translations requires special +permission from their copyright holders, but you may include +translations of some or all Invariant Sections in addition to the +original versions of these Invariant Sections. You may include a +translation of this License, and all the license notices in the +Document, and any Warranty Disclaimers, provided that you also include +the original English version of this License and the original versions +of those notices and disclaimers. In case of a disagreement between +the translation and the original version of this License or a notice +or disclaimer, the original version will prevail. + +If a section in the Document is Entitled "Acknowledgements", +"Dedications", or "History", the requirement (section 4) to Preserve +its Title (section 1) will typically require changing the actual +title. + + +9. TERMINATION + +You may not copy, modify, sublicense, or distribute the Document except +as expressly provided for under this License. Any other attempt to +copy, modify, sublicense or distribute the Document 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. + + +10. FUTURE REVISIONS OF THIS LICENSE + +The Free Software Foundation may publish new, revised versions +of the GNU Free Documentation 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. See +http://www.gnu.org/copyleft/. + +Each version of the License is given a distinguishing version number. +If the Document specifies that a particular numbered version of this +License "or any later version" applies to it, you have the option of +following the terms and conditions either of that specified version or +of any later version that has been published (not as a draft) by the +Free Software Foundation. If the Document does not specify a version +number of this License, you may choose any version ever published (not +as a draft) by the Free Software Foundation. + + +ADDENDUM: How to use this License for your documents + +To use this License in a document you have written, include a copy of +the License in the document and put the following copyright and +license notices just after the title page: + + Copyright (c) YEAR YOUR NAME. + Permission is granted to copy, distribute and/or modify this document + under the terms of the GNU Free Documentation License, Version 1.2 + or any later version published by the Free Software Foundation; + with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. + A copy of the license is included in the section entitled "GNU + Free Documentation License". + +If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, +replace the "with...Texts." line with this: + + with the Invariant Sections being LIST THEIR TITLES, with the + Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST. + +If you have Invariant Sections without Cover Texts, or some other +combination of the three, merge those two alternatives to suit the +situation. + +If your document contains nontrivial examples of program code, we +recommend releasing these examples in parallel under your choice of +free software license, such as the GNU General Public License, +to permit their use in free software. diff --git a/ExtraDesktop.sh b/ExtraDesktop.sh new file mode 100644 index 0000000..11a9429 --- /dev/null +++ b/ExtraDesktop.sh @@ -0,0 +1,4 @@ +#! /bin/sh +#This file outputs in a separate line each file with a .desktop syntax +#that needs to be translated but has a non .desktop extension +find -name \*.kwinrules -print diff --git a/HACKING b/HACKING new file mode 100644 index 0000000..1e0cdc3 --- /dev/null +++ b/HACKING @@ -0,0 +1,5 @@ +Developer documentation can be found in the KDE Community Wiki: + + * http://community.kde.org/KWin - KWin start page + * http://community.kde.org/KWin/Hacking - Hacking information + * http://community.kde.org/KWin/Class_Diagram - Class Diagram 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/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..004d0af --- /dev/null +++ b/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC *.kcfg *.ui >> rc.cpp +$XGETTEXT *.h *.cpp helpers/killer/*.cpp plugins/scenes/opengl/*.cpp tabbox/*.cpp scripting/*.cpp -o $podir/kwin.pot diff --git a/README b/README new file mode 100644 index 0000000..c75f931 --- /dev/null +++ b/README @@ -0,0 +1,209 @@ +- A collection of various documents and links related to KWin is at http://techbase.kde.org/Projects/KWin . + + +- The mailing list for KWin is kwin@kde.org (https://mail.kde.org/mailman/listinfo/kwin). + +- If you want to develop KWin, see file HACKING. + +- If you want to check KWin's compliance with specifications, see file COMPLIANCE. + +- File CONFIGURATION includes some details on configuring KWin. + +- Below is some info for application developers about application interaction + with the window manager, but it'd need some cleanup. + + + + + + + + + This README is meant as an explanation of various window manager related +mechanisms that application developers need to be aware of. As some of these +concepts may be difficult to understand for people not having the required +background knowledge (since sometimes it's difficult even for people who +do have the knowledge), the mechanisms are first briefly explained, and +then an example of fixing the various problems is given. + + For comments, questions, suggestions and whatever use the kwin@kde.org +mailing list. + + +Table of contents: +================== + +- Window relations + - how to make the window manager know which windows belong together +- Focus stealing prevention + - how to solve cases where focus stealing prevention doesn't work + properly automatically + + + +Window relations: +================= + +(For now, this explanation of window relations is mainly meant for +focus stealing prevention. To be extended later.) + + All windows created by an application should be organized in a tree +with the root being the application's main window. Note that this is about +toplevel windows, not widgets inside the windows. For example, if you +have KWrite running, with a torn-off toolbar (i.e. a standalone toolbar), +a file save dialog open, and the file save dialog showing a dialog +for creating a directory, the window hiearchy should look like this: + + + KWrite mainwindow + / \ + / \ + file save dialog torn-off toolbar + \ + \ + create directory dialog + + Each subwindow (i.e. all except for the KWrite mainwindow) points to its +main window (which in turn may have another main window, as in the case +of the file save dialog). When the window manager knows these relations, +it can better arrange the windows (keeping subwindows above their +main windows, preventing activation of a main window of a modal dialog, +and similar). Failing to provide this information to the window manager +may have various results, for example having dialogs positioned below +the main window, + +The window property used by subwindows to point to their mainwindows is +called WM_TRANSIENT_FOR. It can be seen by running +'xprop | grep WM_TRANSIENT_FOR' and clicking on a window. If the property +is not present, the window does not (claim to) have any mainwindow. +If the property is present, it's value is the window id of its main window; +window id of any window can be found out by running 'xwininfo'. A window +having WM_TRANSIENT_FOR poiting to another window is said to be transient +for that window. + + In some cases, the WM_TRANSIENT_FOR property may not point to any other +existing window, having value of 0, or pointing to the screen number +('xwininfo -root'). These special values mean that the window is transient +for all other windows in its window group. This should be used only +in rare cases, everytime a specific main window is known, WM_TRANSIENT_FOR +should be pointing to it instead of using one of these special values. +(The explanation why is beyond the scope of this document - just accept it +as a fact.) + + With Qt, the WM_TRANSIENT_FOR property is set by Qt automatically, based +on the toplevel widget's parent. If the toplevel widget is of a normal +type (i.e. not a dialog, toolbar, etc.), Qt doesn't set WM_TRANSIENT_FOR +on it. For special widgets, such as dialogs, WM_TRANSIENT_FOR is set +to point to the widget's parent, if it has a specific parent, otherwise +WM_TRANSIENT_FOR points to the root window. + + As already said above, WM_TRANSIENT_FOR poiting to the root window should +be usually avoided, so everytime the widget's main widget is known, the widget +should get it passed as a parent in its constructor. +(TODO KDialog etc. classes should not have a default argument for the parent +argument, and comments like 'just pass 0 as the parent' should go.) + + + +Focus stealing prevention: +========================== + + Since KDE3.2 KWin has a feature called focus stealing prevention. As the name +suggests, it prevents unexpected changes of focus. With older versions of KWin, +if any application opened a new dialog, it became active, and +if the application's main window was on another virtual desktop, also +the virtual desktop was changed. This was annoying, and also sometimes led +to dialogs mistakenly being closed because they received keyboard input that +was meant for the previously active window. + + The basic principle of focus stealing prevention is that the window with most +recent user activity wins. Any window of an application will become active +when being shown only if this application was the most recently used one. +KWin itself, and some of the related kdecore classes should take care +of the common cases, so usually there's no need for any special handling +in applications. Qt/KDE applications, that is. Applications using other +toolkits should in most cases work fine too. If they don't support +the window property _NET_WM_USER_TIME, the window manager may fail to detect +the user timestamp properly, resulting either in other windows becoming active +while the user works with this application, or this application may sometimes +steal focus (this second case should be very rare though). + + There are also cases where KDE applications needs special handling. The two +most common cases are when windows relations are not setup properly to make +KWin realize that they belong to the same application, and when the user +activity is not represented by manipulating with the application windows +themselves. + + Also note that focus stealing prevention implemented in the window manager +can only help with focus stealing between different applications. +If an application itself suddenly pops up a dialog, KWin cannot do anything about +it, and its the application's job to handle this case. + + +Window relations: +----------------- + + The common case here is when a dialog is shown for an application, but this +dialog is not provided by the application itself, but by some other process. +For example, dialogs with warnings about accepted cookies are provided +by KCookieJar, instead of being shown by Konqueror. In the normal case, +from KWin's point of view the cookie dialog would be an attempt of another +application to show a dialog, and KWin wouldn't allow activation of this +window. + + The solution is to tell the window manager about the relation between +the Konqueror main window and the cookie dialog, by making the dialog +point to the mainwindow. Note that this is not special to focus stealing +prevention, subwindows such as dialogs, toolbars and similar should always +point to their mainwindow. See the section on window relations for full +description. + + The WM_TRANSIENT_FOR property that's set on dialogs to point to their +mainwindow should in the cookie dialog case point to the Konqueror window +for which it has been shown. This is solved in kcookiejar by including +the window id in the DCOP call. When the cookie dialog is shown, its +WM_TRANSIENT_FOR property is manually set using the XSetTransientForHint() +call (see kdelibs/kioslave/http/kcookiejar/kcookiewin.cpp). The arguments +to XSetTransientForHint() call are the X display (i.e. QX11Info::display()), +the window id on which the WM_TRANSIENT_FOR property is to be set +(i.e. use QWidget::winId()), and the window id of the mainwindow. + + + Simple short HOWTO: + + To put it simply: Let's say you have a daemon application that has +DCOP call "showDialog( QString text )", and when this is called, it shows +a dialog with the given text. This won't work properly with focus stealing +prevention. The DCOP call should be changed to +"showDialog( QString text, long id )". The caller should pass something like +myMainWindow->winId() as the second argument. In the daemon, before +the dialog is shown, a call to XSetTransientHint() should be added: + + XSetTransientForHint( QX11Info::display(), dialog->winId(), id_of_mainwindow ); + + That's it. + +Non-standard user activity: +--------------------------- + + The most common case in KDE will be DCOP calls. For example, KDesktop's DCOP +call "KDesktopIface popupExecuteCommand". Executing this DCOP call e.g. +from Konsole as 'dcop kdesktop KDesktopIface popupExecuteCommand" will lead +to showing the minicli, but the last user activity timestamp gained from events +sent by X server will be older than user activity timestamp of Konsole, and +would normally result in minicli not being active. Therefore, before showing +the minicli, kdesktop needs to call KApplication::updateUserTimestamp(). + + However, this shouldn't be done with all DCOP calls. If a DCOP call is not +a result of direct user action, calling KApplication::updateUserTimestamp() +would lead to focus stealing. For example, let's assume for a moment +that KMail would use this DCOP call in case it detects the modem is not +connected, allowing to you to start KPPP or whatever tool you use. If KMail +would be configured to check mail every 10 minutes, this would lead to minicli +possibly suddenly showing up at every check. Basically, doing the above change +to kdesktop's minicli means that the popupExecuteCommand() DCOP call is only +for user scripting. (TODO write about focus transferring?) + + Simply said, KApplication::updateUserTimestamp() should be called only +as a result of user action. Unfortunately, I'm not aware of any universal +way how to handle this, so every case will have to be considered separately. diff --git a/abstract_client.cpp b/abstract_client.cpp new file mode 100644 index 0000000..ed72b9c --- /dev/null +++ b/abstract_client.cpp @@ -0,0 +1,1899 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 "tabgroup.h" +#include "useractions.h" +#include "workspace.h" + +#include "wayland_server.h" +#include + +#include + +#include + +#include +#include + +namespace KWin +{ + +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::geometryShapeChanged, this, &AbstractClient::geometryChanged); + auto signalMaximizeChanged = static_cast(&AbstractClient::clientMaximizedStateChanged); + connect(this, signalMaximizeChanged, this, &AbstractClient::geometryChanged); + connect(this, &AbstractClient::clientStepUserMovedResized, this, &AbstractClient::geometryChanged); + 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); + + // replace on-screen-display on size changes + connect(this, &AbstractClient::geometryShapeChanged, this, + [this] (Toplevel *c, const QRect &old) { + Q_UNUSED(c) + if (isOnScreenDisplay() && !geometry().isEmpty() && old.size() != geometry().size() && !isInitialPositionSet()) { + GeometryUpdatesBlocker blocker(this); + QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop()); + Placement::self()->place(this, area); + setGeometryRestore(geometry()); + } + } + ); + + connect(this, &AbstractClient::paddingChanged, this, [this]() { + m_visibleRectBeforeGeometryUpdate = visibleRect(); + }); + + connect(ApplicationMenu::self(), &ApplicationMenu::applicationMenuEnabledChanged, this, [this] { + emit hasApplicationMenuChanged(hasApplicationMenu()); + }); +} + +AbstractClient::~AbstractClient() +{ + 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::setTabGroup(TabGroup* group) +{ + tab_group = group; + emit tabGroupChanged(); +} + +void AbstractClient::setClientShown(bool shown) +{ + Q_UNUSED(shown) +} + +bool AbstractClient::untab(const QRect &toGeometry, bool clientRemoved) +{ + TabGroup *group = tab_group; + if (group && group->remove(this)) { // remove sets the tabgroup to "0", therefore the pointer is cached + if (group->isEmpty()) { + delete group; + } + if (clientRemoved) + return true; // there's been a broadcast signal that this client is now removed - don't touch it + setClientShown(!(isMinimized() || isShade())); + bool keepSize = toGeometry.size() == size(); + bool changedSize = false; + if (quickTileMode() != QuickTileMode(QuickTileFlag::None)) { + changedSize = true; + setQuickTileMode(QuickTileFlag::None); // if we leave a quicktiled group, assume that the user wants to untile + } + if (toGeometry.isValid()) { + if (maximizeMode() != MaximizeRestore) { + changedSize = true; + maximize(MaximizeRestore); // explicitly calling for a geometry -> unmaximize + } + if (keepSize && changedSize) { + setGeometryRestore(geometry()); // checkWorkspacePosition() invokes it + QPoint cpoint = Cursor::pos(); + QPoint point = cpoint; + point.setX((point.x() - toGeometry.x()) * geometryRestore().width() / toGeometry.width()); + point.setY((point.y() - toGeometry.y()) * geometryRestore().height() / toGeometry.height()); + auto geometry_restore = geometryRestore(); + geometry_restore.moveTo(cpoint-point); + setGeometryRestore(geometry_restore); + } else { + setGeometryRestore(toGeometry); // checkWorkspacePosition() invokes it + } + setGeometry(geometryRestore()); + checkWorkspacePosition(); + } + return true; + } + return false; +} + +bool AbstractClient::tabTo(AbstractClient *other, bool behind, bool activate) +{ + Q_ASSERT(other && other != this); + + if (tab_group && tab_group == other->tabGroup()) { // special case: move inside group + tab_group->move(this, other, behind); + return true; + } + + GeometryUpdatesBlocker blocker(this); + const bool wasBlocking = signalsBlocked(); + blockSignals(true); // prevent client emitting "retabbed to nowhere" cause it's about to be entabbed the next moment + untab(); + blockSignals(wasBlocking); + + TabGroup *newGroup = other->tabGroup() ? other->tabGroup() : new TabGroup(other); + + if (!newGroup->add(this, other, behind, activate)) { + if (newGroup->count() < 2) { // adding "c" to "to" failed for whatever reason + newGroup->remove(other); + delete newGroup; + } + return false; + } + return true; +} + +void AbstractClient::syncTabGroupFor(QString property, bool fromThisClient) +{ + if (tab_group) + tab_group->sync(property.toAscii().data(), fromThisClient ? this : tab_group->current()); +} + +bool AbstractClient::isCurrentTab() const +{ + return !tab_group || tab_group->current() == this; +} + +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 (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 : NULL); + + 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() +{ +} + +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::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 (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 (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()) { + // force hint change if different + if (info && bool(info->state() & NET::KeepAbove) != keepAbove()) + info->setState(keepAbove() ? NET::KeepAbove : NET::States(0), NET::KeepAbove); + return; + } + m_keepAbove = b; + if (info) { + info->setState(keepAbove() ? NET::KeepAbove : NET::States(0), NET::KeepAbove); + } + workspace()->updateClientLayer(this); + updateWindowRules(Rules::Above); + + doSetKeepAbove(); + 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()) { + // force hint change if different + if (info && bool(info->state() & NET::KeepBelow) != keepBelow()) + info->setState(keepBelow() ? NET::KeepBelow : NET::States(0), NET::KeepBelow); + return; + } + m_keepBelow = b; + if (info) { + info->setState(keepBelow() ? NET::KeepBelow : NET::States(0), NET::KeepBelow); + } + workspace()->updateClientLayer(this); + updateWindowRules(Rules::Below); + + doSetKeepBelow(); + 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::wantsTabFocus() const +{ + return (isNormalWindow() || isDialog()) && wantsInput(); +} + +bool AbstractClient::isSpecialWindow() const +{ + // TODO + return isDesktop() || isDock() || isSplash() || isToolbar() || isNotification() || isOnScreenDisplay(); +} + +void AbstractClient::demandAttention(bool set) +{ + if (isActive()) + set = false; + if (m_demandsAttention == set) + return; + m_demandsAttention = set; + if (info) { + info->setState(set ? NET::DemandsAttention : NET::States(0), NET::DemandsAttention); + } + workspace()->clientAttentionChanged(this, set); + emit demandsAttentionChanged(); +} + +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)); + if (m_desktop == desktop) + return; + + int was_desk = m_desktop; + const bool wasOnCurrentDesktop = isOnCurrentDesktop(); + m_desktop = desktop; + + 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)->setDesktop(desktop); + + 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->setDesktop(desktop); + } + + doSetDesktop(desktop, was_desk); + + FocusChain::self()->update(this, FocusChain::MakeFirst); + updateWindowRules(Rules::Desktop); + + emit desktopChanged(); + if (wasOnCurrentDesktop != isOnCurrentDesktop()) + emit desktopPresenceChanged(this, was_desk); +} + +void AbstractClient::doSetDesktop(int desktop, int was_desk) +{ + Q_UNUSED(desktop) + Q_UNUSED(was_desk) +} + +void AbstractClient::setOnAllDesktops(bool b) +{ + if ((b && isOnAllDesktops()) || + (!b && !isOnAllDesktops())) + return; + if (b) + setDesktop(NET::OnAllDesktops); + else + setDesktop(VirtualDesktopManager::self()->current()); +} + +bool AbstractClient::isShadeable() const +{ + return false; +} + +void AbstractClient::setShade(bool set) +{ + set ? setShade(ShadeNormal) : setShade(ShadeNone); +} + +void AbstractClient::setShade(ShadeMode mode) +{ + Q_UNUSED(mode) +} + +ShadeMode AbstractClient::shadeMode() const +{ + return 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; + + if (isShade() && info) // NETWM restriction - KWindowInfo::isMinimized() == Hidden && !Shaded + info->setState(0, NET::Shaded); + + m_minimized = true; + + doMinimize(); + + updateWindowRules(Rules::Minimize); + FocusChain::self()->update(this, FocusChain::MakeFirstMinimized); + // TODO: merge signal with s_minimized + emit clientMinimized(this, !avoid_animation); + emit minimizedChanged(); +} + +void AbstractClient::unminimize(bool avoid_animation) +{ + if (!isMinimized()) + return; + + if (rules()->checkMinimize(false)) { + return; + } + + if (isShade() && info) // NETWM restriction - KWindowInfo::isMinimized() == Hidden && !Shaded + info->setState(NET::Shaded, NET::Shaded); + + 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(); +} + +void AbstractClient::updateColorScheme(QString path) +{ + if (path.isEmpty()) { + path = QStringLiteral("kdeglobals"); + } + + if (!m_palette || m_colorScheme != path) { + m_colorScheme = path; + + 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()); + } +} + +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(qMin(area.width(), width()), qMin(area.height(), height())); + } + int tx = x(), ty = y(); + if (geometry().right() > area.right() && width() <= area.width()) + tx = area.right() - width() + 1; + if (geometry().bottom() > area.bottom() && height() <= area.height()) + ty = area.bottom() - height() + 1; + if (!area.contains(geometry().topLeft())) { + if (tx < area.x()) + tx = area.x(); + if (ty < area.y()) + ty = area.y(); + } + if (tx != x() || ty != y()) + move(tx, ty); +} + +QSize AbstractClient::maxSize() const +{ + return rules()->checkMaxSize(QSize(INT_MAX, INT_MAX)); +} + +QSize AbstractClient::minSize() const +{ + return rules()->checkMinSize(QSize(0, 0)); +} + +void AbstractClient::updateMoveResize(const QPointF ¤tGlobalCursor) +{ + handleMoveResize(pos(), currentGlobalCursor.toPoint()); +} + +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 KWayland::Server; + auto w = waylandServer()->windowManagement()->createWindow(waylandServer()->windowManagement()); + 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->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 Client::actionSupported(), but both should be implemented. + w->setParentWindow(transientFor() ? transientFor()->windowManagementInterface() : nullptr); + w->setGeometry(geom); + 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::desktopChanged, w, + [w, this] { + if (isOnAllDesktops()) { + w->setOnAllDesktops(true); + return; + } + w->setVirtualDesktop(desktop() - 1); + w->setOnAllDesktops(false); + } + ); + 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::geometryChanged, w, + [w, this] { + w->setGeometry(geom); + } + ); + connect(w, &PlasmaWindowInterface::closeRequested, this, [this] { closeWindow(); }); + connect(w, &PlasmaWindowInterface::moveRequested, this, + [this] { + Cursor::setPos(geometry().center()); + performMouseCommand(Options::MouseMove, Cursor::pos()); + } + ); + connect(w, &PlasmaWindowInterface::resizeRequested, this, + [this] { + Cursor::setPos(geometry().bottomRight()); + performMouseCommand(Options::MouseResize, Cursor::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); + } + ); + 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()) { + *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) { + ToplevelList::const_iterator 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->geometry().intersects(geometry())); + } + } + 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::MousePreviousTab: + if (tabGroup()) + tabGroup()->activatePrev(); + break; + case Options::MouseNextTab: + if (tabGroup()) + tabGroup()->activateNext(); + 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::MouseDragTab: + 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; +} + +QPoint AbstractClient::transientPlacementHint() const +{ + return QPoint(); +} + +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) +{ + assert(!m_transients.contains(cl)); + 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 + +QSize AbstractClient::sizeForClientSize(const QSize &wsize, Sizemode mode, bool noframe) const +{ + Q_UNUSED(mode) + Q_UNUSED(noframe) + return wsize + QSize(borderLeft() + borderRight(), borderTop() + borderBottom()); +} + +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; +} + +void AbstractClient::updateGeometryBeforeUpdateBlocking() +{ + m_geometryBeforeUpdateBlocking = geom; +} + +void AbstractClient::updateTabGroupStates(TabGroup::States) +{ +} + +void AbstractClient::doMove(int, int) +{ +} + +void AbstractClient::updateInitialMoveResizeGeometry() +{ + m_moveResize.initialGeometry = geometry(); + 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()->setClientIsMoving(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::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 = Cursor::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; + } + Cursor::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::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::MidButton) + 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 + && com != Options::MouseDragTab) { + 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::MouseDragTab || + 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) +{ + // TODO: shade hover + 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(); + // TODO: shade hover + // 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()->findAbstractClient(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(); +} + +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 QString(); + } + QString desktopFile = QString::fromUtf8(m_desktopFileName); + if (!desktopFile.endsWith(QLatin1String(".desktop"))) { + desktopFile.append(QLatin1String(".desktop")); + } + KDesktopFile df(desktopFile); + 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(); + + 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(); + + 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); +} + +} diff --git a/abstract_client.h b/abstract_client.h new file mode 100644 index 0000000..f65cf30 --- /dev/null +++ b/abstract_client.h @@ -0,0 +1,1226 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_ABSTRACT_CLIENT_H +#define KWIN_ABSTRACT_CLIENT_H + +#include "toplevel.h" +#include "options.h" +#include "rules.h" +#include "tabgroup.h" +#include "cursor.h" + +#include + +#include +#include + +namespace KWayland +{ +namespace Server +{ +class PlasmaWindowInterface; +} +} + +namespace KDecoration2 +{ +class Decoration; +} + +namespace KWin +{ + +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 the currently visible Client in its Client Group (Window Tabs). + * For change connect to the visibleChanged signal on the Client's Group. + **/ + Q_PROPERTY(bool isCurrentTab READ isCurrentTab) + /** + * 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. + **/ + 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) + /** + * 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 http://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 geometryChanged signal + * might be emitted at each resize step or only at the end of the resize operation. + **/ + Q_PROPERTY(QRect geometry READ geometry WRITE setGeometry) + /** + * 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 "Window Tabs" Group this Client belongs to. + **/ + Q_PROPERTY(KWin::TabGroup* tabGroup READ tabGroup NOTIFY tabGroupChanged SCRIPTABLE false) + +public: + virtual ~AbstractClient(); + + 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 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; + + QPoint clientPos() const override { + return QPoint(borderLeft(), borderTop()); + } + + virtual void updateMouseGrab(); + /** + * @returns The caption consisting of @link{captionNormal} and @link{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 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; + bool isFullScreenable() const; + bool isFullScreenable(bool fullscreen_hack) const; + virtual bool isFullScreen() const = 0; + // 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; + /** + * @returns The recommended position of the transient in parent coordinates + **/ + virtual QPoint transientPlacementHint() 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 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); + virtual bool performMouseCommand(Options::MouseCommand, const QPoint &globalPos); + void setOnAllDesktops(bool set); + void setDesktop(int); + int desktop() const override { + return m_desktop; + } + 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) = 0; + // Tabbing functions + Q_INVOKABLE inline bool tabBefore(AbstractClient *other, bool activate) { return tabTo(other, false, activate); } + Q_INVOKABLE inline bool tabBehind(AbstractClient *other, bool activate) { return tabTo(other, true, activate); } + /** + * Syncs the *dynamic* @param property @param fromThisClient or the @link currentTab() to + * all members of the @link tabGroup() (if there is one) + * + * eg. if you call: + * client->setProperty("kwin_tiling_floats", true); + * client->syncTabGroupFor("kwin_tiling_floats", true) + * all clients in this tabGroup will have ::property("kwin_tiling_floats").toBool() == true + * + * WARNING: non dynamic properties are ignored - you're not supposed to alter/update such explicitly + */ + Q_INVOKABLE void syncTabGroupFor(QString property, bool fromThisClient = false); + TabGroup *tabGroup() const; + /** + * Set tab group - this is to be invoked by TabGroup::add/remove(client) and NO ONE ELSE + */ + void setTabGroup(TabGroup* group); + virtual void setClientShown(bool shown); + Q_INVOKABLE bool untab(const QRect &toGeometry = QRect(), bool clientRemoved = false); + /* + * When a click is done in the decoration and it calls the group + * to change the visible client it starts to move-resize the new + * client, this function stops it. + */ + bool isCurrentTab() const; + virtual QRect geometryRestore() const = 0; + virtual MaximizeMode maximizeMode() const = 0; + void maximize(MaximizeMode); + void setMaximize(bool vertically, bool horizontally); + virtual bool noBorder() const = 0; + virtual void setNoBorder(bool set) = 0; + virtual void blockActivityUpdates(bool b = true) = 0; + QPalette palette() const; + const Decoration::DecorationPalette *decorationPalette() const; + virtual bool isResizable() const = 0; + virtual bool isMovable() const = 0; + virtual bool isMovableAcrossScreens() const = 0; + /** + * @c true only for @c ShadeNormal + **/ + bool isShade() const { + return shadeMode() == ShadeNormal; + } + /** + * Default implementation returns @c ShadeNone + **/ + virtual ShadeMode shadeMode() const; // Prefer isShade() + void setShade(bool set); + /** + * Default implementation does nothing + **/ + virtual void setShade(ShadeMode mode); + /** + * Whether the Client can be shaded. Default implementation returns @c false. + **/ + virtual bool isShadeable() const; + virtual bool isMaximizable() const = 0; + virtual bool isMinimizable() const = 0; + virtual QRect iconGeometry() const; + virtual bool userCanSetFullScreen() const = 0; + virtual bool userCanSetNoBorder() const = 0; + 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(); + void applyWindowRules(); + virtual void 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); + + /** Set 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. + */ + void setQuickTileMode(QuickTileMode mode, bool keyboard = false); + QuickTileMode quickTileMode() const { + return QuickTileMode(m_quickTileMode); + } + Layer layer() const override; + void updateLayer(); + + enum ForceGeometry_t { NormalGeometrySet, ForceGeometrySet }; + void move(int x, int y, ForceGeometry_t force = NormalGeometrySet); + void move(const QPoint &p, ForceGeometry_t force = NormalGeometrySet); + virtual void resizeWithChecks(int w, int h, ForceGeometry_t force = NormalGeometrySet) = 0; + void resizeWithChecks(const QSize& s, ForceGeometry_t force = NormalGeometrySet); + void keepInArea(QRect area, bool partial = false); + virtual QSize minSize() const; + virtual QSize maxSize() const; + virtual void setGeometry(int x, int y, int w, int h, ForceGeometry_t force = NormalGeometrySet) = 0; + void setGeometry(const QRect& r, ForceGeometry_t force = NormalGeometrySet); + /// How to resize the window in order to obey constains (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 + }; + /** + *Calculate the appropriate frame size for the given client size @p wsize. + * + * @p wsize is adapted according to the window's size hints (minimum, maximum and incremental size changes). + * + * Default implementation returns the passed in @p wsize. + */ + virtual QSize sizeForClientSize(const QSize &wsize, Sizemode mode = SizemodeAny, bool noframe = false) const; + + QSize adjustedSize(const QSize&, Sizemode mode = SizemodeAny) const; + QSize adjustedSize() const; + + bool isMove() const { + return isMoveResize() && moveResizePointerMode() == PositionCenter; + } + bool isResize() const { + return isMoveResize() && moveResizePointerMode() != PositionCenter; + } + /** + * Cursor shape for move/resize mode. + **/ + CursorShape cursor() const { + return m_moveResize.cursor; + } + + 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) = 0; + + /** + * 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; + + /** + * 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() = 0; + + 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; + + 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; + } + + /** + * 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; + } + +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 shadeChanged(); + void minimizedChanged(); + void clientMinimized(KWin::AbstractClient* client, bool animate); + void clientUnminimized(KWin::AbstractClient* client, bool animate); + void paletteChanged(const QPalette &p); + 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 hasApplicationMenuChanged(bool); + void applicationMenuActiveChanged(bool); + void unresponsiveChanged(bool); + /** + * Emitted whenever the Client's TabGroup changed. That is whenever the Client is moved to + * another group, but not when a Client gets added or removed to the Client's ClientGroup. + **/ + void tabGroupChanged(); + +protected: + AbstractClient(); + void setFirstInTabBox(bool enable) { + m_firstInTabBox = enable; + } + void setIcon(const QIcon &icon); + void startAutoRaise(); + void autoRaise(); + /** + * 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 ::setDeskop once the desktop value got updated, but before the changed signal + * is emitted. + * + * Default implementation does nothing. + * @param desktop The new desktop the Client is on + * @param was_desk The desktop the Client was on before + **/ + virtual void doSetDesktop(int desktop, int was_desk); + /** + * Called from ::minimize and ::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(); + + void setupWindowManagementInterface(); + void destroyWindowManagementInterface(); + + void updateColorScheme(QString path); + virtual void updateColorScheme() = 0; + + void setTransientFor(AbstractClient *transientFor); + virtual void addTransient(AbstractClient* cl); + /** + * Just removes the @p cl from the transients without any further checks. + **/ + void removeTransientFromList(AbstractClient* cl); + + 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; + } + + KWayland::Server::PlasmaWindowInterface *windowManagementInterface() const { + return m_windowManagementInterface; + } + + // 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) = 0; + virtual void setGeometryRestore(const QRect &geo) = 0; + /** + * 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 geometryBeforeUpdateBlocking() const { + return m_geometryBeforeUpdateBlocking; + } + 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(); + + /** + * Convenient method to update the TabGroup states if there is one present. + * Marked as virtual as TabGroup does not yet handle AbstractClient, but only + * subclasses of AbstractClient. Given that the default implementation does nothing. + **/ + virtual void updateTabGroupStates(TabGroup::States states); + + /** + * @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 @link startMoveResize. + * + * Implementing classes should return @c false if starting move resize should + * get aborted. In that case @link startMoveResize will also return @c false. + * + * Base implementation returns @c true. + **/ + virtual bool doStartMoveResize(); + 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 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 @link{captionNormal} and @link{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); + +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_keepAbove = false; + bool m_keepBelow = false; + bool m_demandsAttention = false; + bool m_minimized = false; + QTimer *m_autoRaiseTimer = nullptr; + int m_desktop = 0; // 0 means not on any desktop yet + + QString m_colorScheme; + std::shared_ptr m_palette; + static QHash> s_palettes; + static std::shared_ptr s_defaultPalette; + + KWayland::Server::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_geometryBeforeUpdateBlocking; + + 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; + TabGroup* tab_group = nullptr; + + 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 void AbstractClient::resizeWithChecks(const QSize& s, AbstractClient::ForceGeometry_t force) +{ + resizeWithChecks(s.width(), s.height(), force); +} + +inline void AbstractClient::setGeometry(const QRect& r, ForceGeometry_t force) +{ + setGeometry(r.x(), r.y(), r.width(), r.height(), 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; +} + +inline TabGroup* AbstractClient::tabGroup() const +{ + return tab_group; +} + +} + +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..c47eb4e --- /dev/null +++ b/abstract_opengl_context_attribute_builder.cpp @@ -0,0 +1,40 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..0028abc --- /dev/null +++ b/abstract_opengl_context_attribute_builder.h @@ -0,0 +1,126 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..81922e6 --- /dev/null +++ b/abstract_output.cpp @@ -0,0 +1,130 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#include "abstract_output.h" +#include "wayland_server.h" + +// KWayland +#include +#include +#include +#include +// KF5 +#include + +#include + +namespace KWin +{ + +AbstractOutput::AbstractOutput(QObject *parent) + : QObject(parent) +{ +} + +AbstractOutput::~AbstractOutput() +{ + delete m_waylandOutputDevice.data(); + delete m_xdgOutput.data(); + delete m_waylandOutput.data(); +} + +QString AbstractOutput::name() const +{ + if (!m_waylandOutput) { + return i18n("unknown"); + } + return QStringLiteral("%1 %2").arg(m_waylandOutput->manufacturer()).arg(m_waylandOutput->model()); +} + +QRect AbstractOutput::geometry() const +{ + return QRect(m_globalPos, pixelSize() / scale()); +} + +QSize AbstractOutput::physicalSize() const +{ + if (m_orientation == Qt::PortraitOrientation || m_orientation == Qt::InvertedPortraitOrientation) { + return m_physicalSize.transposed(); + } + return m_physicalSize; +} + +void AbstractOutput::setGlobalPos(const QPoint &pos) +{ + m_globalPos = pos; + if (m_waylandOutput) { + m_waylandOutput->setGlobalPosition(pos); + } + if (m_waylandOutputDevice) { + m_waylandOutputDevice->setGlobalPosition(pos); + } + if (m_xdgOutput) { + m_xdgOutput->setLogicalPosition(pos); + m_xdgOutput->done(); + } +} + +void AbstractOutput::setScale(qreal scale) +{ + m_scale = scale; + if (m_waylandOutput) { + // 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)); + } + if (m_waylandOutputDevice) { + m_waylandOutputDevice->setScaleF(scale); + } + if (m_xdgOutput) { + m_xdgOutput->setLogicalSize(pixelSize() / m_scale); + m_xdgOutput->done(); + } +} + +void AbstractOutput::setChanges(KWayland::Server::OutputChangeSet *changes) +{ + m_changeset = changes; + qCDebug(KWIN_CORE) << "set changes in AbstractOutput"; + commitChanges(); +} + +void AbstractOutput::setWaylandOutput(KWayland::Server::OutputInterface *set) +{ + m_waylandOutput = set; +} + +void AbstractOutput::createXdgOutput() +{ + if (!m_waylandOutput || m_xdgOutput) { + return; + } + m_xdgOutput = waylandServer()->xdgOutputManager()->createXdgOutput(m_waylandOutput, m_waylandOutput); +} + +void AbstractOutput::setWaylandOutputDevice(KWayland::Server::OutputDeviceInterface *set) +{ + m_waylandOutputDevice = set; +} + +} diff --git a/abstract_output.h b/abstract_output.h new file mode 100644 index 0000000..22bb3ea --- /dev/null +++ b/abstract_output.h @@ -0,0 +1,158 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#ifndef KWIN_OUTPUT_H +#define KWIN_OUTPUT_H + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace KWayland +{ +namespace Server +{ +class OutputInterface; +class OutputDeviceInterface; +class OutputChangeSet; +class OutputManagementInterface; +class XdgOutputInterface; +} +} + +namespace KWin +{ + +namespace ColorCorrect { +struct GammaRamp; +} + +/** + * Generic output representation in a Wayland session + **/ +class KWIN_EXPORT AbstractOutput : public QObject +{ + Q_OBJECT +public: + explicit AbstractOutput(QObject *parent = nullptr); + virtual ~AbstractOutput(); + + QString name() const; + bool isEnabled() const { + return !m_waylandOutput.isNull(); + } + + virtual QSize pixelSize() const = 0; + qreal scale() const { + return m_scale; + } + /* + * The geometry of this output in global compositor co-ordinates (i.e scaled) + */ + QRect geometry() const; + QSize physicalSize() const; + Qt::ScreenOrientation orientation() const { + return m_orientation; + } + + bool isInternal() const { + return m_internal; + } + + void setGlobalPos(const QPoint &pos); + void setScale(qreal scale); + + /** + * This sets the changes and tests them against the specific output + */ + void setChanges(KWayland::Server::OutputChangeSet *changeset); + virtual bool commitChanges() { return false; } + + QPointer waylandOutput() const { + return m_waylandOutput; + } + + virtual int getGammaRampSize() const { + return 0; + } + virtual bool setGammaRamp(const ColorCorrect::GammaRamp &gamma) { + Q_UNUSED(gamma); + return false; + } + +protected: + QPointer changes() const { + return m_changeset; + } + + void setWaylandOutput(KWayland::Server::OutputInterface *set); + + QPointer xdgOutput() const { + return m_xdgOutput; + } + void createXdgOutput(); + + QPointer waylandOutputDevice() const { + return m_waylandOutputDevice; + } + void setWaylandOutputDevice(KWayland::Server::OutputDeviceInterface *set); + + QPoint globalPos() const { + return m_globalPos; + } + + QSize rawPhysicalSize() const { + return m_physicalSize; + } + void setRawPhysicalSize(const QSize &set) { + m_physicalSize = set; + } + + void setOrientation(Qt::ScreenOrientation set) { + m_orientation = set; + } + bool internal() const { + return m_internal; + } + void setInternal(bool set) { + m_internal = set; + } + +private: + QPointer m_changeset; + QPointer m_waylandOutput; + QPointer m_xdgOutput; + QPointer m_waylandOutputDevice; + + QPoint m_globalPos; + qreal m_scale = 1; + QSize m_physicalSize; + Qt::ScreenOrientation m_orientation = Qt::PrimaryOrientation; + bool m_internal = false; +}; + +} + +#endif // KWIN_OUTPUT_H diff --git a/activation.cpp b/activation.cpp new file mode 100644 index 0000000..63e7623 --- /dev/null +++ b/activation.cpp @@ -0,0 +1,893 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ + +/* + + This file contains things relevant to window activation and focus + stealing prevention. + +*/ + +#include "client.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 + +#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 Client::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 Client::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 Client::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(Cursor::pos()); + if (active_client != NULL) { + // note that this may call setActiveClient( NULL ), therefore the recursion counter + active_client->setActive(false); + } + active_client = c; + Q_ASSERT(c == NULL || 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. + + \sa stActiveClient(), requestFocus() + */ +void Workspace::activateClient(AbstractClient* c, bool force) +{ + if (c == NULL) { + focusToNull(); + setActiveClient(NULL); + 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 Client::readUserTimeMapTimestamp(). + if (Client *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. + + \sa Workspace::activateClient() + */ +void Workspace::requestFocus(AbstractClient* c, bool force) +{ + takeActivity(c, force ? ActivityFocusForce : ActivityFocus); +} + +void 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; + } + + if (flags & ActivityFocus) { + AbstractClient* modal = c->findModal(); + if (modal != NULL && 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->tabGroup() && c->tabGroup()->current() != c) + c->tabGroup()->setCurrent(c); + if (!c->isShown(true)) { // shouldn't happen, call activateClient() if needed + qCWarning(KWIN_CORE) << "takeActivity: not shown" ; + return; + } + + if (flags & ActivityFocus) + c->takeFocus(); + if (flags & ActivityRaise) + workspace()->raiseClient(c); + + if (!c->isOnActiveScreen()) + screens()->setCurrent(c->screen()); +} + +/*! + 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. + + \a c may already be destroyed + */ +void Workspace::clientHidden(AbstractClient* c) +{ + assert(!c->isShown(true) || !c->isOnCurrentDesktop() || !c->isOnCurrentActivity()); + activateNextClient(c); +} + +AbstractClient *Workspace::clientUnderMouse(int screen) const +{ + ToplevelList::const_iterator 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->geometry().contains(Cursor::pos())) { + return client; + } + } + return 0; +} + +// 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 != NULL) { + if (c == active_client) + setActiveClient(NULL); + 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 = NULL; + + // precedence on keeping the current tabgroup active. to the user that's the same window + if (c && c->tabGroup() && c->isShown(false)) { + if (c == c->tabGroup()->current()) + c->tabGroup()->activateNext(); + get_focus = c->tabGroup()->current(); + if (get_focus == c) // single tab case - should not happen + get_focus = NULL; + } + + 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 = NULL; + } + } + + 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 == NULL) // last chance: focus the desktop + get_focus = findDesktop(true, desktop); + + if (get_focus != NULL) + 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 == NULL) + get_focus = findDesktop(true, desktop); + if (get_focus != NULL && 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 (session_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 == NULL || 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 + Time 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 (session_saving && level <= 2) { // <= normal + return true; + } + AbstractClient* ac = mostRecentlyActivatedClient(); + if (level == 0) // none + return true; + if (level == 4) // extreme + return false; + if (ac == NULL || 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 Client after FocusIn that wasn't initiated by KWin and the client +// wasn't allowed to activate +void 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) + requestFocus(should_get_focus.last()); + else if (last_active_client) + requestFocus(last_active_client); +} + +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 Client::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 = NULL; // do not hover re-shade a window after it got interaction + } + group()->updateUserTime(m_userTime); +} + +xcb_timestamp_t Client::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 Client::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 != NULL && 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). + Client* act = dynamic_cast(workspace()->mostRecentlyActivatedClient()); + if (act != NULL && !belongToSameApplication(act, this, SameApplicationCheck::RelaxedForActive)) { + bool first_window = true; + auto sameApplicationActiveHackPredicate = [this](const Client *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 && Client::belongToSameApplication(cl, this, SameApplicationCheck::RelaxedForActive); + }; + if (isTransient()) { + auto clientMainClients = [this] () -> ClientList { + ClientList ret; + const auto mcs = mainClients(); + for (auto mc: mcs) { + if (Client *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) == NULL) + ; // 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 Client::userTime() const +{ + xcb_timestamp_t time = m_userTime; + if (time == 0) // doesn't want focus after showing + return 0; + assert(group() != NULL); + if (time == -1U + || (group()->userTime() != -1U + && NET::timestampCompare(group()->userTime(), time) > 0)) + time = group()->userTime(); + return time; +} + +void Client::doSetActive() +{ + updateUrgency(); // demand attention again if it's still urgent +} + +void Client::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()); + Time 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 Client::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 Client::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..42c5db8 --- /dev/null +++ b/activities.cpp @@ -0,0 +1,218 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "activities.h" +// KWin +#include "client.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 = NULL; +} + +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 (Client * client, Workspace::self()->clientList()) { + 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(Client* 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) { + Client *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->sessionSaving()) { + 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()->sessionSaving()) { + 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->sessionSaving()) + return; //ksmserver doesn't queue requests (yet) + + qCDebug(KWIN_CORE) << id; + + QSet saveSessionIds; + QSet dontCloseSessionIds; + const ClientList &clients = ws->clientList(); + for (ClientList::const_iterator it = clients.constBegin(); it != clients.constEnd(); ++it) { + const Client* c = (*it); + 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..c3496b2 --- /dev/null +++ b/activities.h @@ -0,0 +1,129 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_ACTIVITIES_H +#define KWIN_ACTIVITIES_H + +#include + +#include +#include + +#include + +namespace KActivities { +class Controller; +} + +namespace KWin +{ +class Client; + +class KWIN_EXPORT Activities : public QObject +{ + Q_OBJECT + +public: + ~Activities(); + + 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(Client* 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..04e79d8 --- /dev/null +++ b/appmenu.cpp @@ -0,0 +1,131 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (c) 2011 Lionel Chauvin +Copyright (c) 2011,2012 Cédric Bellegarde +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "appmenu.h" +#include "client.h" +#include "workspace.h" +#include + +#include +#include + +#include "decorations/decorationbridge.h" +#include + +using 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(); + }); +} diff --git a/appmenu.h b/appmenu.h new file mode 100644 index 0000000..9e6cee7 --- /dev/null +++ b/appmenu.h @@ -0,0 +1,75 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (c) 2011 Lionel Chauvin +Copyright (c) 2011,2012 Cédric Bellegarde +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..ee9e8e2 --- /dev/null +++ b/atoms.cpp @@ -0,0 +1,84 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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_aware(QByteArrayLiteral("XdndAware")) + , xdnd_position(QByteArrayLiteral("XdndPosition")) + , 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_net_wm_tab_group(QByteArrayLiteral("_KDE_NET_WM_TAB_GROUP")) + , 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")) + , gtk_frame_extents(QByteArrayLiteral("_GTK_FRAME_EXTENTS")) + , kwin_dbus_service(QByteArrayLiteral("_ORG_KDE_KWIN_DBUS_SERVICE")) + , utf8_string(QByteArrayLiteral("UTF8_STRING")) + , 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")) + , 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..2c7b2d9 --- /dev/null +++ b/atoms.h @@ -0,0 +1,91 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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_aware; + Xcb::Atom xdnd_position; + 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_net_wm_tab_group; + 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 gtk_frame_extents; + Xcb::Atom kwin_dbus_service; + Xcb::Atom utf8_string; + Xcb::Atom wl_surface_id; + Xcb::Atom kde_net_wm_appmenu_service_name; + Xcb::Atom kde_net_wm_appmenu_object_path; + + /** + * @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..35b4202 --- /dev/null +++ b/autotests/CMakeLists.txt @@ -0,0 +1,445 @@ +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 + test_virtual_desktops.cpp + ../virtualdesktops.cpp +) +add_executable(testVirtualDesktops ${testVirtualDesktops_SRCS}) + +target_link_libraries( testVirtualDesktops + Qt5::Test + Qt5::Widgets + KF5::I18n + KF5::GlobalAccel + KF5::ConfigCore + KF5::WindowSystem +) +add_test(NAME kwin-testVirtualDesktops COMMAND testVirtualDesktops) +ecm_mark_as_test(testVirtualDesktops) + +######################################################## +# Test ClientMachine +######################################################## +set( testClientMachine_SRCS + test_client_machine.cpp + ../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::X11Extras + Qt5::Widgets + 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::X11Extras + Qt5::Widgets + 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::X11Extras + Qt5::Widgets + KF5::ConfigCore + KF5::WindowSystem + XCB::XCB + XCB::ICCCM + ) + 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::X11Extras + Qt5::Widgets + KF5::ConfigCore + KF5::WindowSystem + XCB::XCB +) +add_test(NAME kwin-testXcbWindow COMMAND testXcbWindow) +ecm_mark_as_test(testXcbWindow) + +######################################################## +# Test BuiltInEffectLoader +######################################################## +set( testBuiltInEffectLoader_SRCS + test_builtin_effectloader.cpp + mock_effectshandler.cpp + ../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 + test_scripted_effectloader.cpp + mock_abstract_client.cpp + mock_effectshandler.cpp + mock_screens.cpp + mock_workspace.cpp + ../effectloader.cpp + ../scripting/scriptedeffect.cpp + ../scripting/scriptingutils.cpp + ../scripting/scripting_logging.cpp + ../screens.cpp + ../orientation_sensor.cpp +) +kconfig_add_kcfg_files(testScriptedEffectLoader_SRCS ../settings.kcfgc) +qt5_add_dbus_adaptor( testScriptedEffectLoader_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/../org.kde.kwin.OrientationSensor.xml ${CMAKE_CURRENT_SOURCE_DIR}/../orientation_sensor.h KWin::OrientationSensor) +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 + test_plugin_effectloader.cpp + mock_effectshandler.cpp + ../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 + test_screens.cpp + mock_abstract_client.cpp + mock_client.cpp + mock_screens.cpp + mock_workspace.cpp + ../screens.cpp + ../x11eventfilter.cpp + ../orientation_sensor.cpp +) +kconfig_add_kcfg_files(testScreens_SRCS ../settings.kcfgc) +qt5_add_dbus_adaptor( testScreens_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/../org.kde.kwin.OrientationSensor.xml ${CMAKE_CURRENT_SOURCE_DIR}/../orientation_sensor.h KWin::OrientationSensor) + +add_executable( testScreens ${testScreens_SRCS}) +target_include_directories(testScreens BEFORE PRIVATE ./) +target_link_libraries(testScreens + Qt5::DBus + Qt5::Sensors + Qt5::Test + Qt5::X11Extras + Qt5::Widgets + KF5::ConfigCore + KF5::ConfigGui + KF5::I18n + KF5::Notifications + KF5::WindowSystem +) + +add_test(NAME kwin_testScreens COMMAND testScreens) +ecm_mark_as_test(testScreens) + +######################################################## +# Test XrandRScreens +######################################################## +set( testXRandRScreens_SRCS + test_xrandr_screens.cpp + mock_abstract_client.cpp + mock_client.cpp + mock_screens.cpp + mock_workspace.cpp + ../screens.cpp + ../plugins/platforms/x11/standalone/screens_xrandr.cpp + ../xcbutils.cpp # init of extensions + ../x11eventfilter.cpp + ../orientation_sensor.cpp +) +kconfig_add_kcfg_files(testXRandRScreens_SRCS ../settings.kcfgc) +qt5_add_dbus_adaptor( testXRandRScreens_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/../org.kde.kwin.OrientationSensor.xml ${CMAKE_CURRENT_SOURCE_DIR}/../orientation_sensor.h KWin::OrientationSensor) +add_executable( testXRandRScreens ${testXRandRScreens_SRCS} ) +target_link_libraries( testXRandRScreens + Qt5::Test + Qt5::DBus + Qt5::Gui + Qt5::Sensors + Qt5::Widgets + KF5::ConfigCore + KF5::ConfigGui + KF5::I18n + KF5::Notifications + KF5::WindowSystem + XCB::XCB + XCB::RANDR + XCB::XFIXES + XCB::SYNC + XCB::COMPOSITE + XCB::DAMAGE + XCB::GLX + XCB::SHM +) + +add_test(NAME kwin-testXRandRScreens COMMAND testXRandRScreens) +ecm_mark_as_test(testXRandRScreens) + +######################################################## +# Test ScreenEdges +######################################################## +set( testScreenEdges_SRCS + test_screen_edges.cpp + mock_abstract_client.cpp + mock_client.cpp + mock_screens.cpp + mock_workspace.cpp + ../atoms.cpp + ../gestures.cpp + ../screens.cpp + ../screenedge.cpp + ../virtualdesktops.cpp + ../xcbutils.cpp # init of extensions + ../plugins/platforms/x11/standalone/edge.cpp + ../orientation_sensor.cpp +) +kconfig_add_kcfg_files(testScreenEdges_SRCS ../settings.kcfgc) +qt5_add_dbus_interface( testScreenEdges_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/../org.freedesktop.ScreenSaver.xml screenlocker_interface) +qt5_add_dbus_adaptor( testScreenEdges_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/../org.kde.kwin.OrientationSensor.xml ${CMAKE_CURRENT_SOURCE_DIR}/../orientation_sensor.h KWin::OrientationSensor) + +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::I18n + KF5::GlobalAccel + KF5::Notifications + KF5::WindowSystem + XCB::XCB + XCB::RANDR + XCB::XFIXES + XCB::SYNC + XCB::COMPOSITE + XCB::DAMAGE + XCB::GLX + XCB::SHM +) + +add_test(NAME kwin_testScreenEdges COMMAND testScreenEdges) +ecm_mark_as_test(testScreenEdges) + +######################################################## +# Test OnScreenNotification +######################################################## +set( testOnScreenNotification_SRCS + onscreennotificationtest.cpp + ../onscreennotification.cpp + ../input_event_spy.cpp +) +add_executable( testOnScreenNotification ${testOnScreenNotification_SRCS}) + +target_link_libraries(testOnScreenNotification + Qt5::Test + Qt5::Widgets # QAction include + Qt5::Quick + KF5::ConfigCore +) + +add_test(NAME kwin-testOnScreenNotification COMMAND testOnScreenNotification) +ecm_mark_as_test(testOnScreenNotification) + +######################################################## +# Test Gestures +######################################################## +set( testGestures_SRCS + test_gestures.cpp + ../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 + Qt5::Test + KF5::CoreAddons + kwin +) +add_test(NAME kwin-testX11TimestampUpdate COMMAND testX11TimestampUpdate) +ecm_mark_as_test(testX11TimestampUpdate) + +set(testOpenGLContextAttributeBuilder_SRCS + opengl_context_attribute_builder_test.cpp + ../abstract_opengl_context_attribute_builder.cpp + ../egl_context_attribute_builder.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 + test_xkb.cpp + ../xkb.cpp +) +add_executable(testXkb ${testXkb_SRCS}) +target_link_libraries(testXkb + Qt5::Test + Qt5::Gui + Qt5::Widgets + KF5::ConfigCore + KF5::WindowSystem + KF5::WaylandServer + 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::Test + Qt5::DBus +) +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/client.h b/autotests/client.h new file mode 100644 index 0000000..b821e1b --- /dev/null +++ b/autotests/client.h @@ -0,0 +1 @@ +#include "mock_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..70123e2 --- /dev/null +++ b/autotests/drm/mock_drm.cpp @@ -0,0 +1,78 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..ae80769 --- /dev/null +++ b/autotests/drm/mock_drm.h @@ -0,0 +1,32 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..384268b --- /dev/null +++ b/autotests/drm/objecttest.cpp @@ -0,0 +1,218 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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 +{ +struct DrmOutput { + 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..73cc7e4 --- /dev/null +++ b/autotests/fakeeffectplugin.cpp @@ -0,0 +1,49 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include + +namespace KWin +{ + +class FakeEffect : public Effect +{ + Q_OBJECT +public: + FakeEffect() {} + virtual ~FakeEffect() {} + + 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..07066d0 --- /dev/null +++ b/autotests/fakeeffectplugin_version.cpp @@ -0,0 +1,50 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include + +namespace KWin +{ + +class FakeVersionEffect : public Effect +{ + Q_OBJECT +public: + FakeVersionEffect() {} + virtual ~FakeVersionEffect() {} +}; + +} // namespace + +class FakeEffectPluginFactory : public KWin::EffectPluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID KPluginFactory_iid FILE "fakeeffectplugin_version.json") + Q_INTERFACES(KPluginFactory) +public: + FakeEffectPluginFactory() {} + ~FakeEffectPluginFactory() {} + 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..dde39ee --- /dev/null +++ b/autotests/integration/CMakeLists.txt @@ -0,0 +1,84 @@ +add_subdirectory(helper) + +add_library(KWinIntegrationTestFramework STATIC kwin_wayland_test.cpp test_helpers.cpp) +target_link_libraries(KWinIntegrationTestFramework kwin Qt5::Test) + +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(WAYLAND_ONLY NAME testStart SRCS start_test.cpp) +integrationTest(WAYLAND_ONLY NAME testTransientNoInput SRCS transient_no_input_test.cpp) +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 testTransientPlacmenet 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 testShellClient SRCS shell_client_test.cpp) +integrationTest(WAYLAND_ONLY NAME testDontCrashNoBorder SRCS dont_crash_no_border.cpp) +integrationTest(NAME testXClipboardSync SRCS xclipboardsync_test.cpp) +integrationTest(WAYLAND_ONLY NAME testSceneOpenGL SRCS scene_opengl_test.cpp generic_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 generic_scene_opengl_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 testVirtualDesktop SRCS virtual_desktop_test.cpp) +integrationTest(WAYLAND_ONLY NAME testShellClientRules SRCS shell_client_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) + +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) + + 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/activities_test.cpp b/autotests/integration/activities_test.cpp new file mode 100644 index 0000000..91c2385 --- /dev/null +++ b/autotests/integration/activities_test.cpp @@ -0,0 +1,162 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "activities.h" +#include "client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.h" +#include "xcbutils.h" +#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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->setUseKActivities(true); + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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() +{ + QProcess::execute(QStringLiteral("kactivitymanagerd"), QStringList{QStringLiteral("stop")}); +} + +void ActivitiesTest::init() +{ + screens()->setCurrent(0); + Cursor::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 creates a Client and sets it on activities which don't exist + // that should result in the window being on all activities + // 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()); + Client *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"))); + + //setting the client to an invalid activities should result in the client being on all activities + client->setOnActivity(QStringLiteral("foo"), true); + QVERIFY(client->isOnAllActivities()); + QVERIFY(!client->activities().contains(QLatin1String("foo"))); + + client->setOnActivities(QStringList{QStringLiteral("foo"), QStringLiteral("bar")}); + QVERIFY(client->isOnAllActivities()); + 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, &Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::ActivitiesTest) +#include "activities_test.moc" diff --git a/autotests/integration/colorcorrect_nightcolor_test.cpp b/autotests/integration/colorcorrect_nightcolor_test.cpp new file mode 100644 index 0000000..84571d6 --- /dev/null +++ b/autotests/integration/colorcorrect_nightcolor_test.cpp @@ -0,0 +1,334 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2017 Roman Gilg + +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, see . +*********************************************************************/ +#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 workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void ColorCorrectNightColorTest::init() +{ +} + +void ColorCorrectNightColorTest::cleanup() +{ +} + +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("notActiveMode2") << "false" << 2 << 5000 << 90. << -180. << "0600" << "1800" << 1 << true; + QTest::newRow("wrongData1") << "fa" << 3 << 7000 << 91. << -181. << "060" << "800" << 999999 << false; + QTest::newRow("wrongData2") << "fa" << 3 << 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("wrongData0") << true << 3 << 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..c20203c --- /dev/null +++ b/autotests/integration/data/anim-data-delete-effect/effect.js @@ -0,0 +1,25 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +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/debug_console_test.cpp b/autotests/integration/debug_console_test.cpp new file mode 100644 index 0000000..399b6d0 --- /dev/null +++ b/autotests/integration/debug_console_test.cpp @@ -0,0 +1,532 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "debug_console.h" +#include "screens.h" +#include "shell_client.h" +#include "wayland_server.h" +#include "xcbutils.h" + +#include +#include +#include +#include +#include + +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_debug_console-0"); + +class DebugConsoleTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanup(); + void topLevelTest_data(); + void topLevelTest(); + void testX11Client(); + void testX11Unmanaged(); + void testWaylandClient_data(); + void testWaylandClient(); + void testInternalWindow(); + void testClosingDebugConsole(); +}; + +void DebugConsoleTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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.start(QStringLiteral("glxgears")); + 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_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; +} + +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()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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); + shellSurface.reset(); + Test::flushWaylandConnection(); + qDebug() << rowsRemovedSpy.count(); + QEXPECT_FAIL("wlShell", "Deleting a ShellSurface does not result in the server removing the ShellClient", Continue); + QVERIFY(rowsRemovedSpy.wait()); + surface.reset(); + + if (rowsRemovedSpy.isEmpty()) { + 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() = 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(); + + QVERIFY(rowsInsertedSpy.wait()); + QCOMPARE(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(); + + QVERIFY(rowsRemovedSpy.wait()); + QCOMPARE(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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + console->show(); + QCOMPARE(console->windowHandle()->isVisible(), true); + QTRY_COMPARE(clientAddedSpy.count(), 1); + ShellClient *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..34eb190 --- /dev/null +++ b/autotests/integration/decoration_input_test.cpp @@ -0,0 +1,895 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "abstract_client.h" +#include "cursor.h" +#include "pointer_input.h" +#include "touch_input.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.h" +#include +#include "decorations/decoratedclient.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_data(); + 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_data(); + void testTouchEvents(); + void testTooltipDoesntEatKeyEvents_data(); + void testTooltipDoesntEatKeyEvents(); + +private: + AbstractClient *showWindow(Test::ShellSurfaceType type); +}; + +#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(Test::ShellSurfaceType type) +{ + 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); + auto shellSurface = Test::createShellSurface(type, 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 workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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(workspaceCreatedSpy.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); + Cursor::setPos(QPoint(640, 512)); +} + +void DecorationInputTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void DecorationInputTest::testAxis_data() +{ + QTest::addColumn("decoPoint"); + QTest::addColumn("expectedSection"); + QTest::addColumn("type"); + + QTest::newRow("topLeft") << QPoint(0, 0) << Qt::TopLeftSection << Test::ShellSurfaceType::WlShell; + QTest::newRow("top") << QPoint(250, 0) << Qt::TopSection << Test::ShellSurfaceType::WlShell; + QTest::newRow("topRight") << QPoint(499, 0) << Qt::TopRightSection << Test::ShellSurfaceType::WlShell; + QTest::newRow("topLeft|xdgv5") << QPoint(0, 0) << Qt::TopLeftSection << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("top|xdgv5") << QPoint(250, 0) << Qt::TopSection << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("topRight|xdgv5") << QPoint(499, 0) << Qt::TopRightSection << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("topLeft|xdgv6") << QPoint(0, 0) << Qt::TopLeftSection << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("top|xdgv6") << QPoint(250, 0) << Qt::TopSection << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("topRight|xdgv6") << QPoint(499, 0) << Qt::TopRightSection << Test::ShellSurfaceType::XdgShellV6; +} + +void DecorationInputTest::testAxis() +{ + QFETCH(Test::ShellSurfaceType, type); + AbstractClient *c = showWindow(type); + 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->geometry().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::addColumn("type"); + + QTest::newRow("topLeft") << QPoint(0, 0) << Qt::TopLeftSection << Test::ShellSurfaceType::WlShell; + QTest::newRow("top") << QPoint(250, 0) << Qt::TopSection << Test::ShellSurfaceType::WlShell; + QTest::newRow("topRight") << QPoint(499, 0) << Qt::TopRightSection << Test::ShellSurfaceType::WlShell; + QTest::newRow("topLeft|xdgv5") << QPoint(0, 0) << Qt::TopLeftSection << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("top|xdgv5") << QPoint(250, 0) << Qt::TopSection << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("topRight|xdgv5") << QPoint(499, 0) << Qt::TopRightSection << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("topLeft|xdgv6") << QPoint(0, 0) << Qt::TopLeftSection << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("top|xdgv6") << QPoint(250, 0) << Qt::TopSection << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("topRight|xdgv6") << QPoint(499, 0) << Qt::TopRightSection << Test::ShellSurfaceType::XdgShellV6; +} + +void KWin::DecorationInputTest::testDoubleClick() +{ + QFETCH(Test::ShellSurfaceType, type); + AbstractClient *c = showWindow(type); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + QVERIFY(!c->isOnAllDesktops()); + quint32 timestamp = 1; + MOTION(QPoint(c->geometry().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::addColumn("type"); + + QTest::newRow("topLeft") << QPoint(10, 10) << Qt::TopLeftSection << Test::ShellSurfaceType::WlShell; + QTest::newRow("top") << QPoint(260, 10) << Qt::TopSection << Test::ShellSurfaceType::WlShell; + QTest::newRow("topRight") << QPoint(509, 10) << Qt::TopRightSection << Test::ShellSurfaceType::WlShell; + QTest::newRow("topLeft|xdgv5") << QPoint(10, 10) << Qt::TopLeftSection << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("top|xdgv5") << QPoint(260, 10) << Qt::TopSection << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("topRight|xdgv5") << QPoint(509, 10) << Qt::TopRightSection << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("topLeft|xdgv6") << QPoint(10, 10) << Qt::TopLeftSection << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("top|xdgv6") << QPoint(260, 10) << Qt::TopSection << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("topRight|xdgv6") << QPoint(509, 10) << Qt::TopRightSection << Test::ShellSurfaceType::XdgShellV6; + +} + +void KWin::DecorationInputTest::testDoubleTap() +{ + QFETCH(Test::ShellSurfaceType, type); + AbstractClient *c = showWindow(type); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + QVERIFY(!c->isOnAllDesktops()); + quint32 timestamp = 1; + const QPoint tapPoint(c->geometry().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_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void DecorationInputTest::testHover() +{ + QFETCH(Test::ShellSurfaceType, type); + AbstractClient *c = showWindow(type); + 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->geometry().center().x(), c->clientPos().y() / 2)); + QCOMPARE(c->cursor(), CursorShape(Qt::ArrowCursor)); + + MOTION(QPoint(c->geometry().x(), 0)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorthWest)); + MOTION(QPoint(c->geometry().x() + c->geometry().width() / 2, 0)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorth)); + MOTION(QPoint(c->geometry().x() + c->geometry().width() - 1, 0)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeNorthEast)); + MOTION(QPoint(c->geometry().x() + c->geometry().width() - 1, c->height() / 2)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeEast)); + MOTION(QPoint(c->geometry().x() + c->geometry().width() - 1, c->height() - 1)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouthEast)); + MOTION(QPoint(c->geometry().x() + c->geometry().width() / 2, c->height() - 1)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouth)); + MOTION(QPoint(c->geometry().x(), c->height() - 1)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeSouthWest)); + MOTION(QPoint(c->geometry().x(), c->height() / 2)); + QCOMPARE(c->cursor(), CursorShape(KWin::ExtendedCursor::SizeWest)); + + MOTION(c->geometry().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::addColumn("type"); + + QTest::newRow("To right") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0) << Test::ShellSurfaceType::WlShell; + QTest::newRow("To left") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0) << Test::ShellSurfaceType::WlShell; + QTest::newRow("To bottom") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30) << Test::ShellSurfaceType::WlShell; + QTest::newRow("To top") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30) << Test::ShellSurfaceType::WlShell; + QTest::newRow("To right|xdgv5") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0) << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("To left|xdgv5") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0) << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("To bottom|xdgv5") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30) << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("To top|xdgv5") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30) << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("To right|xdgv6") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0) << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("To left|xdgv6") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0) << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("To bottom|xdgv6") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30) << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("To top|xdgv6") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30) << Test::ShellSurfaceType::XdgShellV6; +} + +void DecorationInputTest::testPressToMove() +{ + QFETCH(Test::ShellSurfaceType, type); + AbstractClient *c = showWindow(type); + 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->geometry().center().x(), c->y() + c->clientPos().y() / 2)); + QCOMPARE(c->cursor(), CursorShape(Qt::ArrowCursor)); + + PRESS; + QVERIFY(!c->isMove()); + QFETCH(QPoint, offset); + MOTION(QPoint(c->geometry().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->geometry().center().x(), c->y() + c->clientPos().y() / 2) + offset2); + QVERIFY(c->isMove()); + QCOMPARE(startMoveResizedSpy.count(), 2); + QFETCH(QPoint, offset3); + MOTION(QPoint(c->geometry().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::addColumn("type"); + + QTest::newRow("To right") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0) << Test::ShellSurfaceType::WlShell; + QTest::newRow("To left") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0) << Test::ShellSurfaceType::WlShell; + QTest::newRow("To bottom") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30) << Test::ShellSurfaceType::WlShell; + QTest::newRow("To top") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30) << Test::ShellSurfaceType::WlShell; + QTest::newRow("To right|xdgv5") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0) << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("To left|xdgv5") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0) << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("To bottom|xdgv5") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30) << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("To top|xdgv5") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30) << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("To right|xdgv6") << QPoint(10, 0) << QPoint(20, 0) << QPoint(30, 0) << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("To left|xdgv6") << QPoint(-10, 0) << QPoint(-20, 0) << QPoint(-30, 0) << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("To bottom|xdgv6") << QPoint(0, 10) << QPoint(0, 20) << QPoint(0, 30) << Test::ShellSurfaceType::XdgShellV6; + QTest::newRow("To top|xdgv6") << QPoint(0, -10) << QPoint(0, -20) << QPoint(0, -30) << Test::ShellSurfaceType::XdgShellV6; +} + +void DecorationInputTest::testTapToMove() +{ + QFETCH(Test::ShellSurfaceType, type); + AbstractClient *c = showWindow(type); + 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->geometry().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->geometry().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->geometry().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("type"); + QTest::addColumn("edge"); + QTest::addColumn("expectedCursor"); + + QTest::newRow("wlShell - left") << Test::ShellSurfaceType::WlShell << Qt::LeftEdge << Qt::SizeHorCursor; + QTest::newRow("xdgShellV5 - left") << Test::ShellSurfaceType::XdgShellV5 << Qt::LeftEdge << Qt::SizeHorCursor; + QTest::newRow("xdgShellV6 - left") << Test::ShellSurfaceType::XdgShellV6 << Qt::LeftEdge << Qt::SizeHorCursor; + QTest::newRow("wlShell - right") << Test::ShellSurfaceType::WlShell << Qt::RightEdge << Qt::SizeHorCursor; + QTest::newRow("xdgShellV5 - right") << Test::ShellSurfaceType::XdgShellV5 << Qt::RightEdge << Qt::SizeHorCursor; + QTest::newRow("xdgShellV6 - right") << Test::ShellSurfaceType::XdgShellV6 << Qt::RightEdge << Qt::SizeHorCursor; + QTest::newRow("wlShell - bottom") << Test::ShellSurfaceType::WlShell << Qt::BottomEdge << Qt::SizeVerCursor; + QTest::newRow("xdgShellV5 - bottom") << Test::ShellSurfaceType::XdgShellV5 << Qt::BottomEdge << Qt::SizeVerCursor; + QTest::newRow("xdgShellV6 - bottom") << Test::ShellSurfaceType::XdgShellV6 << 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 + QFETCH(Test::ShellSurfaceType, type); + AbstractClient *c = showWindow(type); + QVERIFY(c); + QVERIFY(c->isDecorated()); + QVERIFY(!c->noBorder()); + c->move(screens()->geometry(0).center() - QPoint(c->width()/2, c->height()/2)); + QVERIFY(c->geometry() != c->inputGeometry()); + QVERIFY(c->inputGeometry().contains(c->geometry())); + 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->geometry().x() -1, c->geometry().center().y())); + break; + case Qt::RightEdge: + MOTION(QPoint(c->geometry().x() + c->geometry().width() +1, c->geometry().center().y())); + break; + case Qt::BottomEdge: + MOTION(QPoint(c->geometry().center().x(), c->geometry().y() + c->geometry().height() + 1)); + break; + default: + break; + } + QVERIFY(!c->geometry().contains(KWin::Cursor::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"); + QTest::addColumn("surfaceType"); + + const QString alt = QStringLiteral("Alt"); + const QString meta = QStringLiteral("Meta"); + + const QVector> surfaceTypes{ + {Test::ShellSurfaceType::WlShell, QByteArrayLiteral("WlShell")}, + {Test::ShellSurfaceType::XdgShellV5, QByteArrayLiteral("XdgShellV5")}, + }; + + for (const auto &type: surfaceTypes) { + QTest::newRow("Left Alt + Left Click" + type.second) << KEY_LEFTALT << BTN_LEFT << alt << false << type.first; + QTest::newRow("Left Alt + Right Click" + type.second) << KEY_LEFTALT << BTN_RIGHT << alt << false << type.first; + QTest::newRow("Left Alt + Middle Click" + type.second) << KEY_LEFTALT << BTN_MIDDLE << alt << false << type.first; + QTest::newRow("Right Alt + Left Click" + type.second) << KEY_RIGHTALT << BTN_LEFT << alt << false << type.first; + QTest::newRow("Right Alt + Right Click" + type.second) << KEY_RIGHTALT << BTN_RIGHT << alt << false << type.first; + QTest::newRow("Right Alt + Middle Click" + type.second) << KEY_RIGHTALT << BTN_MIDDLE << alt << false << type.first; + // now everything with meta + QTest::newRow("Left Meta + Left Click" + type.second) << KEY_LEFTMETA << BTN_LEFT << meta << false << type.first; + QTest::newRow("Left Meta + Right Click" + type.second) << KEY_LEFTMETA << BTN_RIGHT << meta << false << type.first; + QTest::newRow("Left Meta + Middle Click" + type.second) << KEY_LEFTMETA << BTN_MIDDLE << meta << false << type.first; + QTest::newRow("Right Meta + Left Click" + type.second) << KEY_RIGHTMETA << BTN_LEFT << meta << false << type.first; + QTest::newRow("Right Meta + Right Click" + type.second) << KEY_RIGHTMETA << BTN_RIGHT << meta << false << type.first; + QTest::newRow("Right Meta + Middle Click" + type.second) << KEY_RIGHTMETA << BTN_MIDDLE << meta << false << type.first; + + // and with capslock + QTest::newRow("Left Alt + Left Click/CapsLock" + type.second) << KEY_LEFTALT << BTN_LEFT << alt << true << type.first; + QTest::newRow("Left Alt + Right Click/CapsLock" + type.second) << KEY_LEFTALT << BTN_RIGHT << alt << true << type.first; + QTest::newRow("Left Alt + Middle Click/CapsLock" + type.second) << KEY_LEFTALT << BTN_MIDDLE << alt << true << type.first; + QTest::newRow("Right Alt + Left Click/CapsLock" + type.second) << KEY_RIGHTALT << BTN_LEFT << alt << true << type.first; + QTest::newRow("Right Alt + Right Click/CapsLock" + type.second) << KEY_RIGHTALT << BTN_RIGHT << alt << true << type.first; + QTest::newRow("Right Alt + Middle Click/CapsLock" + type.second) << KEY_RIGHTALT << BTN_MIDDLE << alt << true << type.first; + // now everything with meta + QTest::newRow("Left Meta + Left Click/CapsLock" + type.second) << KEY_LEFTMETA << BTN_LEFT << meta << true << type.first; + QTest::newRow("Left Meta + Right Click/CapsLock" + type.second) << KEY_LEFTMETA << BTN_RIGHT << meta << true << type.first; + QTest::newRow("Left Meta + Middle Click/CapsLock" + type.second) << KEY_LEFTMETA << BTN_MIDDLE << meta << true << type.first; + QTest::newRow("Right Meta + Left Click/CapsLock" + type.second) << KEY_RIGHTMETA << BTN_LEFT << meta << true << type.first; + QTest::newRow("Right Meta + Right Click/CapsLock" + type.second) << KEY_RIGHTMETA << BTN_RIGHT << meta << true << type.first; + QTest::newRow("Right Meta + Middle Click/CapsLock" + type.second) << KEY_RIGHTMETA << BTN_MIDDLE << meta << true << type.first; + } +} + +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 + QFETCH(Test::ShellSurfaceType, surfaceType); + AbstractClient *c = showWindow(surfaceType); + 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 + Cursor::setPos(QPoint(c->geometry().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"); + QTest::addColumn("surfaceType"); + + const QString alt = QStringLiteral("Alt"); + const QString meta = QStringLiteral("Meta"); + + const QVector> surfaceTypes{ + {Test::ShellSurfaceType::WlShell, QByteArrayLiteral("WlShell")}, + {Test::ShellSurfaceType::XdgShellV5, QByteArrayLiteral("XdgShellV5")}, + }; + + for (const auto &type: surfaceTypes) { + QTest::newRow("Left Alt" + type.second) << KEY_LEFTALT << alt << false << type.first; + QTest::newRow("Right Alt" + type.second) << KEY_RIGHTALT << alt << false << type.first; + QTest::newRow("Left Meta" + type.second) << KEY_LEFTMETA << meta << false << type.first; + QTest::newRow("Right Meta" + type.second) << KEY_RIGHTMETA << meta << false << type.first; + QTest::newRow("Left Alt/CapsLock" + type.second) << KEY_LEFTALT << alt << true << type.first; + QTest::newRow("Right Alt/CapsLock" + type.second) << KEY_RIGHTALT << alt << true << type.first; + QTest::newRow("Left Meta/CapsLock" + type.second) << KEY_LEFTMETA << meta << true << type.first; + QTest::newRow("Right Meta/CapsLock" + type.second) << KEY_RIGHTMETA << meta << true << type.first; + } +} + +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(); + + QFETCH(Test::ShellSurfaceType, surfaceType); + AbstractClient *c = showWindow(surfaceType); + 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 + Cursor::setPos(QPoint(c->geometry().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++); + } +} + +void DecorationInputTest::testTouchEvents_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +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 + QFETCH(Test::ShellSurfaceType, type); + AbstractClient *c = showWindow(type); + 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->geometry().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 + Cursor::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(), 4); + QCOMPARE(hoverLeaveSpy.count(), 1); +} + +void DecorationInputTest::testTooltipDoesntEatKeyEvents_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +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()); + + QFETCH(Test::ShellSurfaceType, type); + AbstractClient *c = showWindow(type); + 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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + c->decoratedClient()->requestShowToolTip(QStringLiteral("test")); + // now we should get an internal window + QVERIFY(clientAddedSpy.wait()); + ShellClient *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..9d29c1f --- /dev/null +++ b/autotests/integration/desktop_window_x11_test.cpp @@ -0,0 +1,178 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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); + Cursor::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, defaultScreen()->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()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->windowType(), NET::Desktop); + QCOMPARE(client->geometry(), 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, &Client::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..811728e --- /dev/null +++ b/autotests/integration/dont_crash_aurorae_destroy_deco.cpp @@ -0,0 +1,155 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "client.h" +#include "cursor.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.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() +{ + if (!QFile::exists(QStringLiteral("/dev/dri/card0"))) { + QSKIP("Needs a dri device"); + } + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + 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(workspaceCreatedSpy.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 DontCrashAuroraeDestroyDecoTest::init() +{ + screens()->setCurrent(0); + Cursor::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()); + Client *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->geometry().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, &Client::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..9fe7c82 --- /dev/null +++ b/autotests/integration/dont_crash_cancel_animation.cpp @@ -0,0 +1,126 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "abstract_client.h" +#include "client.h" +#include "composite.h" +#include "deleted.h" +#include "effects.h" +#include "effectloader.h" +#include "screens.h" +#include "shell_client.h" +#include "wayland_server.h" +#include "workspace.h" +#include "scripting/scriptedeffect.h" + +#include + +#include +#include +#include +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_dont_crash_cancel_animation-0"); + +class DontCrashCancelAnimationFromAnimationEndedTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testScript(); +}; + +void DontCrashCancelAnimationFromAnimationEndedTest::initTestCase() +{ + qRegisterMetaType(); + 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); + ShellSurface *shellSurface = Test::createShellSurface(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..b6e5a3c --- /dev/null +++ b/autotests/integration/dont_crash_cursor_physical_size_empty.cpp @@ -0,0 +1,124 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2018 Martin Flöser + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "composite.h" +#include "effectloader.h" +#include "client.h" +#include "cursor.h" +#include "effects.h" +#include "platform.h" +#include "shell_client.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_crash_cursor_physical_size_empty-0"); + +class DontCrashCursorPhysicalSizeEmpty : public QObject +{ +Q_OBJECT +private Q_SLOTS: + void init(); + void initTestCase(); + void cleanup(); + void testMoveCursorOverDeco_data(); + void testMoveCursorOverDeco(); +}; + +void DontCrashCursorPhysicalSizeEmpty::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); + + screens()->setCurrent(0); + KWin::Cursor::setPos(QPoint(640, 512)); +} + +void DontCrashCursorPhysicalSizeEmpty::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void DontCrashCursorPhysicalSizeEmpty::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.wait()); +} + +void DontCrashCursorPhysicalSizeEmpty::testMoveCursorOverDeco_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +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()); + QFETCH(Test::ShellSurfaceType, type); + Test::waylandServerSideDecoration()->create(surface.data(), surface.data()); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isDecorated()); + + // destroy physical size + KWayland::Server::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::Cursor::self()->themeChanged(); + + KWin::Cursor::setPos(QPoint(c->geometry().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..d800ef1 --- /dev/null +++ b/autotests/integration/dont_crash_empty_deco.cpp @@ -0,0 +1,121 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "client.h" +#include "cursor.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.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() +{ + if (!QFile::exists(QStringLiteral("/dev/dri/card0"))) { + QSKIP("Needs a dri device"); + } + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // this test needs to enforce OpenGL compositing to get into the crashy condition + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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 DontCrashEmptyDecorationTest::init() +{ + screens()->setCurrent(0); + Cursor::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()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->isDecorated()); + + // let's set a stupid geometry + client->setGeometry(0, 0, 0, 0); + QCOMPARE(client->geometry(), 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, &Client::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..a394324 --- /dev/null +++ b/autotests/integration/dont_crash_glxgears.cpp @@ -0,0 +1,104 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "abstract_client.h" +#include "client.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 workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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.start(QStringLiteral("glxgears")); + QVERIFY(glxgears.waitForStarted()); + + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + QCOMPARE(workspace()->clientList().count(), 1); + Client *glxgearsClient = workspace()->clientList().first(); + QVERIFY(glxgearsClient->isDecorated()); + QSignalSpy closedSpy(glxgearsClient, &Client::windowClosed); + QVERIFY(closedSpy.isValid()); + KDecoration2::Decoration *decoration = glxgearsClient->decoration(); + QVERIFY(decoration); + + // send a mouse event to the position of the close button + QPointF pos = decoration->rect().topRight() + QPointF(-decoration->borderRight() * 2, decoration->borderRight() * 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..e08d6b3 --- /dev/null +++ b/autotests/integration/dont_crash_no_border.cpp @@ -0,0 +1,134 @@ + +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "client.h" +#include "cursor.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.h" +#include + +#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_data(); + void testCreateWindow(); +}; + +void DontCrashNoBorder::initTestCase() +{ + if (!QFile::exists(QStringLiteral("/dev/dri/card0"))) { + QSKIP("Needs a dri device"); + } + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + 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(workspaceCreatedSpy.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 DontCrashNoBorder::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); + + screens()->setCurrent(0); + Cursor::setPos(QPoint(640, 512)); +} + +void DontCrashNoBorder::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void DontCrashNoBorder::testCreateWindow_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void DontCrashNoBorder::testCreateWindow() +{ + // create a window and ensure that this doesn't crash + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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_useractions_menu.cpp b/autotests/integration/dont_crash_useractions_menu.cpp new file mode 100644 index 0000000..31994b6 --- /dev/null +++ b/autotests/integration/dont_crash_useractions_menu.cpp @@ -0,0 +1,117 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "cursor.h" +#include "keyboard_input.h" +#include "platform.h" +#include "pointer_input.h" +#include "shell_client.h" +#include "screens.h" +#include "useractions.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_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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // force style to breeze as that's the one which triggered the crash + QVERIFY(kwinApp()->setStyle(QStringLiteral("breeze"))); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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::Cursor::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::createShellSurface(Test::ShellSurfaceType::WlShell, 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..0297f2c --- /dev/null +++ b/autotests/integration/effects/CMakeLists.txt @@ -0,0 +1,8 @@ +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(NAME testFade SRCS fade_test.cpp) +integrationTest(WAYLAND_ONLY NAME testEffectWindowGeometry SRCS windowgeometry_test.cpp) +integrationTest(NAME testScriptedEffects SRCS scripted_effects_test.cpp) diff --git a/autotests/integration/effects/fade_test.cpp b/autotests/integration/effects/fade_test.cpp new file mode 100644 index 0000000..cdc5f0d --- /dev/null +++ b/autotests/integration/effects/fade_test.cpp @@ -0,0 +1,182 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "composite.h" +#include "effects.h" +#include "effectloader.h" +#include "cursor.h" +#include "platform.h" +#include "shell_client.h" +#include "wayland_server.h" +#include "workspace.h" +#include "effect_builtins.h" + +#include + +#include +#include +#include + +using namespace KWin; +using namespace KWayland::Client; +static const QString s_socketName = QStringLiteral("wayland_test_effects_translucency-0"); + +class FadeTest : public QObject +{ +Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testWindowCloseAfterWindowHidden_data(); + void testWindowCloseAfterWindowHidden(); + +private: + Effect *m_fadeEffect = nullptr; +}; + +void FadeTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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_EFFECTS_FORCE_ANIMATIONS", "1"); + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + QVERIFY(KWin::Compositor::self()); +} + +void FadeTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + + // 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_fade"))); + QVERIFY(e->loadEffect(QStringLiteral("kwin4_effect_fade"))); + QVERIFY(e->isEffectLoaded(QStringLiteral("kwin4_effect_fade"))); + + QCOMPARE(effectLoadedSpy.count(), 1); + m_fadeEffect = effectLoadedSpy.first().first().value(); + QVERIFY(m_fadeEffect); +} + +void FadeTest::cleanup() +{ + Test::destroyWaylandConnection(); + EffectsHandlerImpl *e = static_cast(effects); + if (e->isEffectLoaded(QStringLiteral("kwin4_effect_fade"))) { + e->unloadEffect(QStringLiteral("kwin4_effect_fade")); + } + QVERIFY(!e->isEffectLoaded(QStringLiteral("kwin4_effect_fade"))); + m_fadeEffect = nullptr; +} + +void FadeTest::testWindowCloseAfterWindowHidden_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void FadeTest::testWindowCloseAfterWindowHidden() +{ + // this test simulates the showing/hiding/closing of a Wayland window + // especially the situation that a window got unmapped and destroyed way later + QVERIFY(!m_fadeEffect->isActive()); + + QSignalSpy windowAddedSpy(effects, &EffectsHandler::windowAdded); + QVERIFY(windowAddedSpy.isValid()); + QSignalSpy windowHiddenSpy(effects, &EffectsHandler::windowHidden); + QVERIFY(windowHiddenSpy.isValid()); + QSignalSpy windowShownSpy(effects, &EffectsHandler::windowShown); + QVERIFY(windowShownSpy.isValid()); + QSignalSpy windowClosedSpy(effects, &EffectsHandler::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QTRY_COMPARE(windowAddedSpy.count(), 1); + QTRY_COMPARE(m_fadeEffect->isActive(), true); + + QTest::qWait(500); + QTRY_COMPARE(m_fadeEffect->isActive(), false); + + // now unmap the surface + surface->attachBuffer(Buffer::Ptr()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(windowHiddenSpy.wait()); + QCOMPARE(m_fadeEffect->isActive(), true); + QTest::qWait(500); + QTRY_COMPARE(m_fadeEffect->isActive(), false); + + // and map again + Test::render(surface.data(), QSize(100, 50), Qt::red); + QVERIFY(windowShownSpy.wait()); + QTRY_COMPARE(m_fadeEffect->isActive(), true); + QTest::qWait(500); + QTRY_COMPARE(m_fadeEffect->isActive(), false); + + // and unmap once more + surface->attachBuffer(Buffer::Ptr()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(windowHiddenSpy.wait()); + QCOMPARE(m_fadeEffect->isActive(), true); + QTest::qWait(500); + QTRY_COMPARE(m_fadeEffect->isActive(), false); + + // and now destroy + shellSurface.reset(); + surface.reset(); + QVERIFY(windowClosedSpy.wait()); + QCOMPARE(m_fadeEffect->isActive(), false); +} + +WAYLANDTEST_MAIN(FadeTest) +#include "fade_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..45083c5 --- /dev/null +++ b/autotests/integration/effects/scripted_effects_test.cpp @@ -0,0 +1,355 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2018 David Edmundson + +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, see . +*********************************************************************/ + +#include "scripting/scriptedeffect.h" +#include "libkwineffects/anidata_p.h" + +#include "composite.h" +#include "cursor.h" +#include "cursor.h" +#include "effect_builtins.h" +#include "effectloader.h" +#include "effects.h" +#include "kwin_wayland_test.h" +#include "platform.h" +#include "shell_client.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; +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(); +private: + ScriptedEffect *loadEffect(const QString &name); +}; + +class ScriptedEffectWithDebugSpy : public KWin::ScriptedEffect +{ + Q_OBJECT +public: + ScriptedEffectWithDebugSpy(); + bool load(const QString &name); + 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 workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.wait()); + QVERIFY(Compositor::self()); + + KWin::VirtualDesktopManager::self()->setCount(2); +} + +void ScriptedEffectsTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void ScriptedEffectsTest::cleanup() +{ + Test::destroyWaylandConnection(); + auto *e = static_cast(effects); + while (!e->loadedEffects().isEmpty()) { + const QString effect = e->loadedEffects().first(); + e->unloadEffect(effect); + QVERIFY(!e->isEffectLoaded(effect)); + } +} + +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, this](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::createXdgShellV6Surface(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::createXdgShellV6Surface(surface, surface); + QVERIFY(shellSurface); + shellSurface->setTitle("Window 1"); + auto *c = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + + // we are running the event loop during renderAndWaitForShown + // some time will pass with the event loop running between the window being added and getting to here + // anim.duration is an aboslute value, but retarget will update the duration based on time passed + int timePassed = 0; + + { + const AnimationEffect::AniMap 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].duration, 100); + QCOMPARE(animationsForWindow[0].to, FPx2(1.4)); + QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale); + QCOMPARE(animationsForWindow[0].curve.type(), QEasingCurve::OutQuad); + QCOMPARE(animationsForWindow[0].keepAtTarget, false); + timePassed = animationsForWindow[0].time; + if (animationCount == 2) { + QCOMPARE(animationsForWindow[1].duration, 100); + QCOMPARE(animationsForWindow[1].to, FPx2(0.0)); + QCOMPARE(animationsForWindow[1].attribute, AnimationEffect::Opacity); + QCOMPARE(animationsForWindow[1].keepAtTarget, false); + } + } + QCOMPARE(effectOutputSpy[0].first(), "true"); + + // window state changes, scale should be retargetted + + c->setMinimized(true); + { + const AnimationEffect::AniMap state = effect->state(); + QCOMPARE(state.count(), 1); + const auto &animationsForWindow = state.first().first; + QCOMPARE(animationsForWindow.count(), animationCount); + QCOMPARE(animationsForWindow[0].duration, 200 + timePassed); + QCOMPARE(animationsForWindow[0].to, FPx2(1.5)); + QCOMPARE(animationsForWindow[0].attribute, AnimationEffect::Scale); + QCOMPARE(animationsForWindow[0].keepAtTarget, false); + if (animationCount == 2) { + QCOMPARE(animationsForWindow[1].duration, 200 + timePassed); + QCOMPARE(animationsForWindow[1].to, FPx2(1.5)); + QCOMPARE(animationsForWindow[1].attribute, AnimationEffect::Opacity); + QCOMPARE(animationsForWindow[1].keepAtTarget, false); + } + } + c->setMinimized(false); + { + const AnimationEffect::AniMap 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); +} + +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..5944f53 --- /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.OutQuad); + 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..3f000ca --- /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.OutQuad + }, { + type: Effect.Opacity, + curve: QEasingCurve.OutQuad, + 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/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/screenEdgeTest.js b/autotests/integration/effects/scripts/screenEdgeTest.js new file mode 100644 index 0000000..645137c --- /dev/null +++ b/autotests/integration/effects/scripts/screenEdgeTest.js @@ -0,0 +1,3 @@ +registerScreenEdge(1, function() { + sendTestResponse("triggered"); +}); diff --git a/autotests/integration/effects/scripts/screenEdgeTouchTest.js b/autotests/integration/effects/scripts/screenEdgeTouchTest.js new file mode 100644 index 0000000..6107f69 --- /dev/null +++ b/autotests/integration/effects/scripts/screenEdgeTouchTest.js @@ -0,0 +1,3 @@ +registerTouchScreenEdge(1, function() { + sendTestResponse("triggered"); +}); diff --git a/autotests/integration/effects/scripts/shortcutsTest.js b/autotests/integration/effects/scripts/shortcutsTest.js new file mode 100644 index 0000000..0e3fe7e --- /dev/null +++ b/autotests/integration/effects/scripts/shortcutsTest.js @@ -0,0 +1,3 @@ +registerShortcut("testShortcut", "Test Shortcut", "Meta+Shift+Y", function() { + sendTestResponse("shortcutTriggered"); +}); diff --git a/autotests/integration/effects/slidingpopups_test.cpp b/autotests/integration/effects/slidingpopups_test.cpp new file mode 100644 index 0000000..a67eb40 --- /dev/null +++ b/autotests/integration/effects/slidingpopups_test.cpp @@ -0,0 +1,371 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "client.h" +#include "composite.h" +#include "effects.h" +#include "effectloader.h" +#include "cursor.h" +#include "platform.h" +#include "shell_client.h" +#include "wayland_server.h" +#include "workspace.h" +#include "effect_builtins.h" + +#include + +#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() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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); + + if (QFile::exists(QStringLiteral("/dev/dri/card0"))) { + qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2")); + } + qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", "1"); + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + QVERIFY(Compositor::self()); +} + +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")}; + + 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()); + Client *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, &Client::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")}; + + 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::createShellSurface(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, &Client::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/translucency_test.cpp b/autotests/integration/effects/translucency_test.cpp new file mode 100644 index 0000000..0a6c750 --- /dev/null +++ b/autotests/integration/effects/translucency_test.cpp @@ -0,0 +1,249 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "client.h" +#include "composite.h" +#include "effects.h" +#include "effectloader.h" +#include "cursor.h" +#include "platform.h" +#include "shell_client.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() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.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()); + Client *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::Cursor::setPos(client->geometry().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, &Client::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()); + Client *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, &Client::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..18e5d3c --- /dev/null +++ b/autotests/integration/effects/windowgeometry_test.cpp @@ -0,0 +1,99 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "composite.h" +#include "effects.h" +#include "effectloader.h" +#include "cursor.h" +#include "platform.h" +#include "shell_client.h" +#include "wayland_server.h" +#include "workspace.h" +#include "effect_builtins.h" + +#include + +#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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.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..5693da3 --- /dev/null +++ b/autotests/integration/effects/wobbly_shade_test.cpp @@ -0,0 +1,198 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2018 Martin Flöser + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "client.h" +#include "composite.h" +#include "cursor.h" +#include "effects.h" +#include "effectloader.h" +#include "cursor.h" +#include "platform.h" +#include "shell_client.h" +#include "wayland_server.h" +#include "workspace.h" +#include "effect_builtins.h" + +#include + +#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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.wait()); + QVERIFY(Compositor::self()); +} + +void WobblyWindowsShadeTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); +} + +void WobblyWindowsShadeTest::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 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()); + Client *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()->getMovingClient() == nullptr); + QCOMPARE(client->isMove(), false); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->getMovingClient(), 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::Cursor::pos()); + + // wait for frame rendered + QTest::qWait(100); + + client->keyPressEvent(Qt::Key_Right); + client->updateMoveResize(KWin::Cursor::pos()); + + // wait for frame rendered + QTest::qWait(100); + + client->keyPressEvent(Qt::Key_Down | Qt::ALT); + client->updateMoveResize(KWin::Cursor::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..ddf28a2 --- /dev/null +++ b/autotests/integration/fakes/org.kde.kdecoration2/fakedecoration_with_shadows.cpp @@ -0,0 +1,72 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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..f556017 --- /dev/null +++ b/autotests/integration/generic_scene_opengl_test.cpp @@ -0,0 +1,117 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "generic_scene_opengl_test.h" +#include "composite.h" +#include "effectloader.h" +#include "cursor.h" +#include "platform.h" +#include "scene.h" +#include "shell_client.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() +{ + if (!QFile::exists(QStringLiteral("/dev/dri/card0"))) { + QSKIP("Needs a dri device"); + } + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.wait()); + QVERIFY(Compositor::self()); +} + +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()->slotReinitialize(); + if (sceneCreatedSpy.isEmpty()) { + QVERIFY(sceneCreatedSpy.wait()); + } + QCOMPARE(sceneCreatedSpy.count(), 1); + auto scene = KWin::Compositor::self()->scene(); + QVERIFY(scene); + QCOMPARE(scene->compositingType(), KWin::OpenGL2Compositing); + + // 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..3e053f3 --- /dev/null +++ b/autotests/integration/generic_scene_opengl_test.h @@ -0,0 +1,40 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..c17721b --- /dev/null +++ b/autotests/integration/globalshortcuts_test.cpp @@ -0,0 +1,382 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "client.h" +#include "cursor.h" +#include "input.h" +#include "platform.h" +#include "screens.h" +#include "shell_client.h" +#include "useractions.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_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 workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void GlobalShortcutsTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + screens()->setCurrent(0); + KWin::Cursor::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::createShellSurface(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()); + Client *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, &Client::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::createShellSurface(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::createShellSurface(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(waylandServer(), &WaylandServer::shellClientAdded); + 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..c27ec3c --- /dev/null +++ b/autotests/integration/helper/copy.cpp @@ -0,0 +1,71 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include +#include +#include +#include +#include + +class Window : public QRasterWindow +{ + Q_OBJECT +public: + explicit Window(); + virtual ~Window(); + +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..a27f385 --- /dev/null +++ b/autotests/integration/helper/kill.cpp @@ -0,0 +1,44 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(); + + //after showing the window block the main thread + //1 as we want it to come after the singleshots in qApp construction + QTimer::singleShot(1, []() { + //block + while(true) { + sleep(100000); + } + }); + + return app.exec(); +} diff --git a/autotests/integration/helper/paste.cpp b/autotests/integration/helper/paste.cpp new file mode 100644 index 0000000..0f1428c --- /dev/null +++ b/autotests/integration/helper/paste.cpp @@ -0,0 +1,68 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include +#include +#include +#include +#include + +class Window : public QRasterWindow +{ + Q_OBJECT +public: + explicit Window(); + virtual ~Window(); + +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..bf3d653 --- /dev/null +++ b/autotests/integration/idle_inhibition_test.cpp @@ -0,0 +1,129 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "shell_client.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include + +#include +#include + +using namespace KWin; +using namespace KWayland::Client; +using KWayland::Server::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_data(); + void testInhibit(); +}; + +void TestIdleInhibition::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void TestIdleInhibition::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::IdleInhibition)); + +} + +void TestIdleInhibition::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestIdleInhibition::testInhibit_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +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()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + + // not yet inhibited + QVERIFY(!idle->isInhibited()); + + // now create inhibition on window + QScopedPointer inhibitor(Test::waylandIdleInhibitManager()->createInhibitor(surface.data())); + QVERIFY(inhibitor->isValid()); + // this should inhibit our server object + QVERIFY(inhibitedSpy.wait()); + 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(); + if (type == Test::ShellSurfaceType::WlShell) { + surface.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..e4c3d39 --- /dev/null +++ b/autotests/integration/input_stacking_order.cpp @@ -0,0 +1,190 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 "shell_client.h" +#include + +#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_data(); + void testPointerFocusUpdatesOnStackingOrderChange(); + +private: + void render(KWayland::Client::Surface *surface); +}; + +void InputStackingOrderTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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); + Cursor::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_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; +} + +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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface1 = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface1); + QFETCH(Test::ShellSurfaceType, type); + auto shellSurface1 = Test::createShellSurface(type, surface1, surface1); + QVERIFY(shellSurface1); + render(surface1); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window1 = workspace()->activeClient(); + QVERIFY(window1); + + Surface *surface2 = Test::createSurface(Test::waylandCompositor()); + QVERIFY(surface2); + auto shellSurface2 = Test::createShellSurface(type, 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->geometry(), window2->geometry()); + + // 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..a1fb1a6 --- /dev/null +++ b/autotests/integration/internal_window.cpp @@ -0,0 +1,677 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "cursor.h" +#include "shell_client.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include + +#include +#include +#include +#include + +#include + +using namespace KWayland::Client; + +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(); +}; + +class HelperWindow : public QRasterWindow +{ + Q_OBJECT +public: + HelperWindow(); + ~HelperWindow(); + + 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 workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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() +{ + Cursor::setPos(QPoint(1280, 512)); + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandKeyboard()); +} + +void InternalWindowTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void InternalWindowTest::testEnterLeave() +{ + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + QVERIFY(!workspace()->findToplevel(nullptr)); + QVERIFY(!workspace()->findToplevel(&win)); + win.setGeometry(0, 0, 100, 100); + win.show(); + + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + QVERIFY(!workspace()->activeClient()); + ShellClient *c = clientAddedSpy.first().first().value(); + QVERIFY(c->isInternal()); + QCOMPARE(c->icon().name(), QStringLiteral("wayland")); + QVERIFY(!c->isDecorated()); + QCOMPARE(workspace()->findToplevel(&win), c); + QCOMPARE(c->geometry(), 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(enterSpy.count(), 1); + + kwinApp()->platform()->pointerMotion(QPoint(60, 50), timestamp++); + QTRY_COMPARE(moveSpy.count(), 1); + QCOMPARE(moveSpy.first().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); + + // hide the window, which should be removed from the stacking order + win.hide(); + QTRY_VERIFY(!c->isShown(false)); + QVERIFY(!workspace()->xStackingOrder().contains(c)); + + // show again + win.show(); + QTRY_VERIFY(c->isShown(false)); + QVERIFY(workspace()->xStackingOrder().contains(c)); +} + +void InternalWindowTest::testPointerPressRelease() +{ + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + 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()); + + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QSignalSpy wheelSpy(&win, &HelperWindow::wheel); + QVERIFY(wheelSpy.isValid()); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(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(waylandServer(), &WaylandServer::shellClientAdded); + 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()); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(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(waylandServer(), &WaylandServer::shellClientAdded); + 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()); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(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::createShellSurface(Test::ShellSurfaceType::WlShell, 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(waylandServer(), &WaylandServer::shellClientAdded); + 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()); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(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++); +} + +void InternalWindowTest::testTouch() +{ + // touch events for internal windows are emulated through mouse events + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.show(); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(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 ShellClient + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setOpacity(0.5); + win.setGeometry(0, 0, 100, 100); + win.show(); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QVERIFY(internalClient->isInternal()); + QCOMPARE(internalClient->opacity(), 0.5); + + QSignalSpy opacityChangedSpy(internalClient, &ShellClient::opacityChanged); + QVERIFY(opacityChangedSpy.isValid()); + win.setOpacity(0.75); + QCOMPARE(opacityChangedSpy.count(), 1); + QCOMPARE(internalClient->opacity(), 0.75); +} + +void InternalWindowTest::testMove() +{ + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setOpacity(0.5); + win.setGeometry(0, 0, 100, 100); + win.show(); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QCOMPARE(internalClient->geometry(), QRect(0, 0, 100, 100)); + + // normal move should be synced + internalClient->move(5, 10); + QCOMPARE(internalClient->geometry(), 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->geometry(), 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(waylandServer(), &WaylandServer::shellClientAdded); + 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(); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.setFlags(win.flags() & ~Qt::FramelessWindowHint); + win.show(); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QVERIFY(internalClient->isDecorated()); + + KConfigGroup group = kwinApp()->config()->group("MouseBindings"); + group.writeEntry("CommandAllKey", "Alt"); + group.writeEntry("CommandAll1", "Move"); + group.writeEntry("CommandAll2", "Move"); + group.writeEntry("CommandAll3", "Move"); + group.sync(); + workspace()->slotReconfigure(); + QCOMPARE(options->commandAllModifier(), Qt::AltModifier); + QCOMPARE(options->commandAll1(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll2(), Options::MouseUnrestrictedMove); + QCOMPARE(options->commandAll3(), Options::MouseUnrestrictedMove); + + // move cursor on window + Cursor::setPos(internalClient->geometry().center()); + + // simulate modifier+click + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); + QVERIFY(!internalClient->isMove()); + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + QVERIFY(internalClient->isMove()); + // release modifier should not change it + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, 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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + HelperWindow win; + win.setGeometry(0, 0, 100, 100); + win.setFlags(win.flags() & ~Qt::FramelessWindowHint); + win.show(); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + auto internalClient = clientAddedSpy.first().first().value(); + QVERIFY(internalClient); + QVERIFY(internalClient->isDecorated()); + + KConfigGroup group = kwinApp()->config()->group("MouseBindings"); + group.writeEntry("CommandAllKey", "Alt"); + group.writeEntry("CommandAllWheel", "change opacity"); + group.sync(); + workspace()->slotReconfigure(); + + // move cursor on window + Cursor::setPos(internalClient->geometry().center()); + + // set the opacity to 0.5 + internalClient->setOpacity(0.5); + QCOMPARE(internalClient->opacity(), 0.5); + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, 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_LEFTALT, timestamp++); +} + +} + +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..65f1251 --- /dev/null +++ b/autotests/integration/keyboard_layout_test.cpp @@ -0,0 +1,461 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "keyboard_input.h" +#include "keyboard_layout.h" +#include "platform.h" +#include "shell_client.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#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 +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testReconfigure(); + void testChangeLayoutThroughDBus(); + void testPerLayoutShortcut(); + void testDBusServiceExport(); + void testVirtualDesktopPolicy(); + void testWindowPolicy(); + void testApplicationPolicy(); + +private: + void reconfigureLayouts(); +}; + +void KeyboardLayoutTest::reconfigureLayouts() +{ + // create DBus signal to reload + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/Layouts"), QStringLiteral("org.kde.keyboard"), QStringLiteral("reloadConfig")); + QDBusConnection::sessionBus().send(message); +} + +void KeyboardLayoutTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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)); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void KeyboardLayoutTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void KeyboardLayoutTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +class LayoutChangedSignalWrapper : public QObject +{ + Q_OBJECT +public: + LayoutChangedSignalWrapper() + : QObject() + { + 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 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 + QTRY_COMPARE(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 + KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout"); + layoutGroup.writeEntry("LayoutList", QStringLiteral("de,us,de(neo)")); + layoutGroup.sync(); + reconfigureLayouts(); + // now we should have two layouts + auto xkb = input()->keyboard()->xkb(); + QTRY_COMPARE(xkb->numberOfLayouts(), 3u); + // default layout is German + xkb->switchToLayout(0); + QCOMPARE(xkb->layoutName(), QStringLiteral("German")); + + LayoutChangedSignalWrapper wrapper; + QSignalSpy layoutChangedSpy(&wrapper, &LayoutChangedSignalWrapper::layoutChanged); + QVERIFY(layoutChangedSpy.isValid()); + + // now change through DBus to english + auto 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); + }; + auto reply = changeLayout(QStringLiteral("English (US)")); + reply.waitForFinished(); + QVERIFY(!reply.isError()); + QCOMPARE(reply.reply().arguments().first().toBool(), true); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 1); + layoutChangedSpy.clear(); + + // switch to a layout which does not exist + reply = changeLayout(QStringLiteral("French")); + QVERIFY(!reply.isError()); + QCOMPARE(reply.reply().arguments().first().toBool(), false); + QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + QVERIFY(!layoutChangedSpy.wait()); + 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")); + QVERIFY(layoutChangedSpy.wait()); + 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()); + QVERIFY(layoutChangedSpy.isEmpty()); +} + +void KeyboardLayoutTest::testPerLayoutShortcut() +{ + // this test verifies that per-layout global shortcuts are working correctly. + // first configure layouts + KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout"); + 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; + + reconfigureLayouts(); + // now we should have three layouts + auto xkb = input()->keyboard()->xkb(); + QTRY_COMPARE(xkb->numberOfLayouts(), 3u); + // default layout is English + xkb->switchToLayout(0); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + LayoutChangedSignalWrapper wrapper; + QSignalSpy layoutChangedSpy(&wrapper, &LayoutChangedSignalWrapper::layoutChanged); + QVERIFY(layoutChangedSpy.isValid()); + + // 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 + KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout"); + layoutGroup.writeEntry("LayoutList", QStringLiteral("us")); + layoutGroup.sync(); + reconfigureLayouts(); + auto xkb = input()->keyboard()->xkb(); + QTRY_COMPARE(xkb->numberOfLayouts(), 1u); + // default layout is English + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + // with one layout we should not have the dbus interface + QTRY_VERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.keyboard")).value()); + + // reconfigure to two layouts + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de")); + layoutGroup.sync(); + reconfigureLayouts(); + QTRY_COMPARE(xkb->numberOfLayouts(), 2u); + QTRY_VERIFY(QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.keyboard")).value()); + + // and back to one layout + layoutGroup.writeEntry("LayoutList", QStringLiteral("us")); + layoutGroup.sync(); + reconfigureLayouts(); + QTRY_COMPARE(xkb->numberOfLayouts(), 1u); + QTRY_VERIFY(!QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.keyboard")).value()); +} + +void KeyboardLayoutTest::testVirtualDesktopPolicy() +{ + KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout"); + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)")); + layoutGroup.writeEntry("SwitchMode", QStringLiteral("Desktop")); + layoutGroup.sync(); + reconfigureLayouts(); + auto xkb = input()->keyboard()->xkb(); + QTRY_COMPARE(xkb->numberOfLayouts(), 3u); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + VirtualDesktopManager::self()->setCount(4); + QCOMPARE(VirtualDesktopManager::self()->count(), 4u); + + auto 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); + }; + auto reply = changeLayout(QStringLiteral("German")); + reply.waitForFinished(); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German")); + + // switch to another virtual desktop + auto desktops = VirtualDesktopManager::self()->desktops(); + QCOMPARE(desktops.count(), 4); + QCOMPARE(desktops.first(), VirtualDesktopManager::self()->currentDesktop()); + VirtualDesktopManager::self()->setCurrent(desktops.at(1)); + QCOMPARE(desktops.at(1), VirtualDesktopManager::self()->currentDesktop()); + // should be reset to English + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)"));reply = changeLayout(QStringLiteral("German (Neo 2)")); + reply.waitForFinished(); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + // back to desktop 0 -> German + VirtualDesktopManager::self()->setCurrent(desktops.at(0)); + QCOMPARE(desktops.first(), VirtualDesktopManager::self()->currentDesktop()); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German")); + // desktop 2 -> English + VirtualDesktopManager::self()->setCurrent(desktops.at(2)); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + // desktop 1 -> Neo + VirtualDesktopManager::self()->setCurrent(desktops.at(1)); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + // remove virtual desktops + VirtualDesktopManager::self()->setCount(1); + QTRY_COMPARE(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()); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + +} + +void KeyboardLayoutTest::testWindowPolicy() +{ + KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout"); + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)")); + layoutGroup.writeEntry("SwitchMode", QStringLiteral("Window")); + layoutGroup.sync(); + reconfigureLayouts(); + auto xkb = input()->keyboard()->xkb(); + QTRY_COMPARE(xkb->numberOfLayouts(), 3u); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + // create a window + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + auto c1 = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); + QVERIFY(c1); + + // now switch layout + auto 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); + }; + auto reply = changeLayout(QStringLiteral("German")); + reply.waitForFinished(); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German")); + + // create a second window + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createShellSurface(surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 100), Qt::red); + QVERIFY(c2); + // this should have switched back to English + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + // now change to another layout + reply = changeLayout(QStringLiteral("German (Neo 2)")); + reply.waitForFinished(); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + // activate other window + workspace()->activateClient(c1); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German")); + workspace()->activateClient(c2); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); +} + +void KeyboardLayoutTest::testApplicationPolicy() +{ + KConfigGroup layoutGroup = kwinApp()->kxkbConfig()->group("Layout"); + layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de,de(neo)")); + layoutGroup.writeEntry("SwitchMode", QStringLiteral("WinClass")); + layoutGroup.sync(); + reconfigureLayouts(); + auto xkb = input()->keyboard()->xkb(); + QTRY_COMPARE(xkb->numberOfLayouts(), 3u); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("English (US)")); + + // create a window + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + auto c1 = Test::renderAndWaitForShown(surface.data(), QSize(100, 100), Qt::blue); + QVERIFY(c1); + + // now switch layout + auto 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); + }; + LayoutChangedSignalWrapper wrapper; + QSignalSpy layoutChangedSpy(&wrapper, &LayoutChangedSignalWrapper::layoutChanged); + QVERIFY(layoutChangedSpy.isValid()); + auto reply = changeLayout(QStringLiteral("German")); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 1); + reply.waitForFinished(); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German")); + + // create a second window + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createShellSurface(surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 100), Qt::red); + QVERIFY(c2); + // it is the same application and should not switch the layout + QVERIFY(!layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 1); + // now change to another layout + reply = changeLayout(QStringLiteral("German (Neo 2)")); + QVERIFY(layoutChangedSpy.wait()); + QCOMPARE(layoutChangedSpy.count(), 2); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + // activate other window + workspace()->activateClient(c1); + QVERIFY(!layoutChangedSpy.wait()); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + workspace()->activateClient(c2); + QVERIFY(!layoutChangedSpy.wait()); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); + + shellSurface2.reset(); + surface2.reset(); + QVERIFY(Test::waitForWindowDestroyed(c2)); + QVERIFY(!layoutChangedSpy.wait()); + QTRY_COMPARE(xkb->layoutName(), QStringLiteral("German (Neo 2)")); +} + +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..dd8d7b6 --- /dev/null +++ b/autotests/integration/keymap_creation_failure_test.cpp @@ -0,0 +1,102 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "keyboard_input.h" +#include "keyboard_layout.h" +#include "platform.h" +#include "shell_client.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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.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..6bacd59 --- /dev/null +++ b/autotests/integration/kwin_wayland_test.cpp @@ -0,0 +1,297 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 + +#include +#include +#include +#include +#include +#include + +// system +#include +#include +#include + +namespace KWin +{ + +static void readDisplay(int pipe); + +WaylandTestApplication::WaylandTestApplication(OperationMode mode, int &argc, char **argv) + : Application(mode, argc, argv) +{ + QStandardPaths::setTestModeEnabled(true); + QIcon::setThemeName(QStringLiteral("breeze")); +#ifdef KWIN_BUILD_ACTIVITIES + setUseKActivities(false); +#endif + qputenv("KWIN_COMPOSE", QByteArrayLiteral("Q")); + initPlatform(KPluginMetaData(QStringLiteral("KWinWaylandVirtualBackend.so"))); + WaylandServer::create(this); +} + +WaylandTestApplication::~WaylandTestApplication() +{ + 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(); + } + destroyWorkspace(); + waylandServer()->dispatch(); + disconnect(m_xwaylandFailConnection); + if (x11Connection()) { + Xcb::setInputFocus(XCB_INPUT_FOCUS_POINTER_ROOT); + destroyAtoms(); + emit x11ConnectionAboutToBeDestroyed(); + xcb_disconnect(x11Connection()); + setX11Connection(nullptr); + } + if (m_xwaylandProcess) { + m_xwaylandProcess->terminate(); + while (m_xwaylandProcess->state() != QProcess::NotRunning) { + processEvents(QEventLoop::WaitForMoreEvents); + } + waylandServer()->destroyXWaylandConnection(); + } + if (QStyle *s = style()) { + s->unpolish(this); + } + waylandServer()->terminateClientConnections(); + destroyCompositor(); +} + +void WaylandTestApplication::performStartup() +{ + // 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(); + + if (operationMode() == OperationModeWaylandOnly) { + createCompositor(); + connect(Compositor::self(), &Compositor::sceneCreated, this, &WaylandTestApplication::continueStartupWithSceen); + return; + } + createCompositor(); + connect(Compositor::self(), &Compositor::sceneCreated, this, &WaylandTestApplication::startXwaylandServer); +} + +void WaylandTestApplication::continueStartupWithSceen() +{ + disconnect(Compositor::self(), &Compositor::sceneCreated, this, &WaylandTestApplication::continueStartupWithSceen); + createWorkspace(); +} + +void WaylandTestApplication::continueStartupWithX() +{ + createX11Connection(); + xcb_connection_t *c = x11Connection(); + if (!c) { + // about to quit + return; + } + QSocketNotifier *notifier = new QSocketNotifier(xcb_get_file_descriptor(c), QSocketNotifier::Read, this); + auto processXcbEvents = [this, c] { + while (auto event = xcb_poll_for_event(c)) { + updateX11Time(event); + long result = 0; + if (QThread::currentThread()->eventDispatcher()->filterNativeEvent(QByteArrayLiteral("xcb_generic_event_t"), event, &result)) { + free(event); + continue; + } + if (Workspace::self()) { + Workspace::self()->workspaceEvent(event); + } + free(event); + } + xcb_flush(c); + }; + connect(notifier, &QSocketNotifier::activated, this, processXcbEvents); + connect(QThread::currentThread()->eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, processXcbEvents); + connect(QThread::currentThread()->eventDispatcher(), &QAbstractEventDispatcher::awake, this, processXcbEvents); + + // create selection owner for WM_S0 - magic X display number expected by XWayland + KSelectionOwner owner("WM_S0", c, x11RootWindow()); + owner.claim(true); + + createAtoms(); + + setupEventFilters(); + + // 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()) { + ::exit(1); + } + + createWorkspace(); + + Xcb::sync(); // Trigger possible errors, there's still a chance to abort +} + +void WaylandTestApplication::createX11Connection() +{ + int screenNumber = 0; + xcb_connection_t *c = nullptr; + if (m_xcbConnectionFd == -1) { + c = xcb_connect(nullptr, &screenNumber); + } else { + c = xcb_connect_to_fd(m_xcbConnectionFd, nullptr); + } + if (int error = xcb_connection_has_error(c)) { + std::cerr << "FATAL ERROR: Creating connection to XServer failed: " << error << std::endl; + exit(1); + return; + } + setX11Connection(c); + // we don't support X11 multi-head in Wayland + setX11ScreenNumber(screenNumber); + setX11RootWindow(defaultScreen()->root); +} + +void WaylandTestApplication::startXwaylandServer() +{ + disconnect(Compositor::self(), &Compositor::sceneCreated, this, &WaylandTestApplication::startXwaylandServer); + int pipeFds[2]; + if (pipe(pipeFds) != 0) { + std::cerr << "FATAL ERROR failed to create pipe to start Xwayland " << std::endl; + exit(1); + return; + } + int sx[2]; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sx) < 0) { + std::cerr << "FATAL ERROR: failed to open socket to open XCB connection" << std::endl; + exit(1); + return; + } + int fd = dup(sx[1]); + if (fd < 0) { + std::cerr << "FATAL ERROR: failed to open socket to open XCB connection" << std::endl; + exit(20); + return; + } + + const int waylandSocket = waylandServer()->createXWaylandConnection(); + if (waylandSocket == -1) { + std::cerr << "FATAL ERROR: failed to open socket for Xwayland" << std::endl; + exit(1); + return; + } + const int wlfd = dup(waylandSocket); + if (wlfd < 0) { + std::cerr << "FATAL ERROR: failed to open socket for Xwayland" << std::endl; + exit(20); + return; + } + + m_xcbConnectionFd = sx[0]; + + m_xwaylandProcess = new QProcess(kwinApp()); + m_xwaylandProcess->setProgram(QStringLiteral("Xwayland")); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + env.insert("WAYLAND_SOCKET", QByteArray::number(wlfd)); + m_xwaylandProcess->setProcessEnvironment(env); + m_xwaylandProcess->setArguments({QStringLiteral("-displayfd"), + QString::number(pipeFds[1]), + QStringLiteral("-rootless"), + QStringLiteral("-wm"), + QString::number(fd)}); + m_xwaylandFailConnection = connect(m_xwaylandProcess, static_cast(&QProcess::error), this, + [] (QProcess::ProcessError error) { + if (error == QProcess::FailedToStart) { + std::cerr << "FATAL ERROR: failed to start Xwayland" << std::endl; + } else { + std::cerr << "FATAL ERROR: Xwayland failed, going to exit now" << std::endl; + } + exit(1); + } + ); + const int xDisplayPipe = pipeFds[0]; + connect(m_xwaylandProcess, &QProcess::started, this, + [this, xDisplayPipe] { + QFutureWatcher *watcher = new QFutureWatcher(this); + QObject::connect(watcher, &QFutureWatcher::finished, this, &WaylandTestApplication::continueStartupWithX, Qt::QueuedConnection); + QObject::connect(watcher, &QFutureWatcher::finished, watcher, &QFutureWatcher::deleteLater, Qt::QueuedConnection); + watcher->setFuture(QtConcurrent::run(readDisplay, xDisplayPipe)); + } + ); + m_xwaylandProcess->start(); + close(pipeFds[1]); +} + +static void readDisplay(int pipe) +{ + QFile readPipe; + if (!readPipe.open(pipe, QIODevice::ReadOnly)) { + std::cerr << "FATAL ERROR failed to open pipe to start X Server" << std::endl; + exit(1); + } + QByteArray displayNumber = readPipe.readLine(); + + displayNumber.prepend(QByteArray(":")); + displayNumber.remove(displayNumber.size() -1, 1); + std::cout << "X-Server started on display " << displayNumber.constData() << std::endl; + + setenv("DISPLAY", displayNumber.constData(), true); + + // close our pipe + close(pipe); +} + +} diff --git a/autotests/integration/kwin_wayland_test.h b/autotests/integration/kwin_wayland_test.h new file mode 100644 index 0000000..8a1ad5f --- /dev/null +++ b/autotests/integration/kwin_wayland_test.h @@ -0,0 +1,208 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_WAYLAND_TEST_H +#define KWIN_WAYLAND_TEST_H + +#include "../../main.h" + +// Qt +#include + +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 Shell; +class ShellSurface; +class ShmPool; +class Surface; +class XdgShellSurface; +} +} + +namespace KWin +{ + +class AbstractClient; +class ShellClient; + +class WaylandTestApplication : public Application +{ + Q_OBJECT +public: + WaylandTestApplication(OperationMode mode, int &argc, char **argv); + virtual ~WaylandTestApplication(); + +protected: + void performStartup() override; + +private: + void createBackend(); + void createX11Connection(); + void continueStartupWithScreens(); + void continueStartupWithSceen(); + void continueStartupWithX(); + void startXwaylandServer(); + + int m_xcbConnectionFd = -1; + QProcess *m_xwaylandProcess = nullptr; + QMetaObject::Connection m_xwaylandFailConnection; +}; + +namespace Test +{ + +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 +}; +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::ShadowManager *waylandShadowManager(); +KWayland::Client::Shell *waylandShell(); +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(); + + +bool waitForWaylandPointer(); +bool waitForWaylandTouch(); +bool waitForWaylandKeyboard(); + +void flushWaylandConnection(); + +KWayland::Client::Surface *createSurface(QObject *parent = nullptr); +enum class ShellSurfaceType { + WlShell, + XdgShellV5, + XdgShellV6 +}; +QObject *createShellSurface(ShellSurfaceType type, KWayland::Client::Surface *surface, QObject *parent = nullptr); +KWayland::Client::ShellSurface *createShellSurface(KWayland::Client::Surface *surface, QObject *parent = nullptr); +KWayland::Client::XdgShellSurface *createXdgShellV5Surface(KWayland::Client::Surface *surface, QObject *parent = nullptr); +KWayland::Client::XdgShellSurface *createXdgShellV6Surface(KWayland::Client::Surface *surface, QObject *parent = nullptr); + + +/** + * 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 ShellClient is shown and returns the created ShellClient. + * If no ShellClient gets shown during @p timeout @c null is returned. + **/ +ShellClient *waitForWaylandWindowShown(int timeout = 5000); + +/** + * Combination of @link{render} and @link{waitForWaylandWindowShown}. + **/ +ShellClient *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::ShellSurfaceType) + +#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); \ + DPI; \ + KWin::WaylandTestApplication app(OperationMode, argc, argv); \ + app.setAttribute(Qt::AA_Use96Dpi, true); \ + const auto ownPath = app.libraryPaths().last(); \ + app.removeLibraryPath(ownPath); \ + app.addLibraryPath(ownPath); \ + TestObject tc; \ + return QTest::qExec(&tc, argc, argv); \ +} + +#ifdef NO_XWAYLAND +#define WAYLANDTEST_MAIN(TestObject) WAYLANDTEST_MAIN_HELPER(TestObject, QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling), KWin::Application::OperationModeWaylandOnly) +#else +#define WAYLANDTEST_MAIN(TestObject) WAYLANDTEST_MAIN_HELPER(TestObject, QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling), 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..499b114 --- /dev/null +++ b/autotests/integration/kwinbindings_test.cpp @@ -0,0 +1,267 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "cursor.h" +#include "input.h" +#include "platform.h" +#include "screens.h" +#include "shell_client.h" +#include "scripting/scripting.h" +#include "useractions.h" +#include "virtualdesktops.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void KWinBindingsTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + screens()->setCurrent(0); + KWin::Cursor::setPos(QPoint(640, 512)); +} + +void KWinBindingsTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void KWinBindingsTest::testSwitchWindow() +{ + // first create windows + QScopedPointer surface1(Test::createSurface()); + QScopedPointer shellSurface1(Test::createShellSurface(surface1.data())); + auto c1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createShellSurface(surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createShellSurface(surface3.data())); + auto c3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface4(Test::createSurface()); + QScopedPointer shellSurface4(Test::createShellSurface(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::createShellSurface(surface1.data())); + auto c1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createShellSurface(surface2.data())); + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface3(Test::createSurface()); + QScopedPointer shellSurface3(Test::createShellSurface(surface3.data())); + auto c3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface4(Test::createSurface()); + QScopedPointer shellSurface4(Test::createShellSurface(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::createShellSurface(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/lockscreen.cpp b/autotests/integration/lockscreen.cpp new file mode 100644 index 0000000..30ceaff --- /dev/null +++ b/autotests/integration/lockscreen.cpp @@ -0,0 +1,764 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 "shell_client.h" +#include + +#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 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; + KWayland::Client::Shell *m_shell = nullptr; +}; + +class HelperEffect : public Effect +{ + Q_OBJECT +public: + HelperEffect() {} + ~HelperEffect() {} + + 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); + ShellSurface *shellSurface = Test::createShellSurface(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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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 LockScreenTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat)); + QVERIFY(Test::waitForWaylandPointer()); + m_connection = Test::waylandConnection(); + m_compositor = Test::waylandCompositor(); + m_shell = Test::waylandShell(); + m_shm = Test::waylandShmPool(); + m_seat = Test::waylandSeat(); + + screens()->setCurrent(0); + Cursor::setPos(QPoint(640, 512)); +} + +void LockScreenTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +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->geometry().center()); + QVERIFY(enteredSpy.wait()); + + LOCK + + QVERIFY(leftSpy.wait()); + QCOMPARE(leftSpy.count(), 1); + + // simulate moving out in and out again + MOTION(c->geometry().center()); + MOTION(c->geometry().bottomRight() + QPoint(100, 100)); + MOTION(c->geometry().bottomRight() + QPoint(100, 100)); + QVERIFY(!leftSpy.wait()); + QCOMPARE(leftSpy.count(), 1); + QCOMPARE(enteredSpy.count(), 1); + + // go back on the window + MOTION(c->geometry().center()); + // and unlock + UNLOCK + + QVERIFY(enteredSpy.wait()); + QCOMPARE(enteredSpy.count(), 2); + // move on the window + MOTION(c->geometry().center() + QPoint(100, 100)); + QVERIFY(leftSpy.wait()); + MOTION(c->geometry().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->geometry().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->geometry().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()->getMovingClient(), 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()->getMovingClient(), c); + QVERIFY(c->isMove()); + kwinApp()->platform()->keyboardKeyPressed(KEY_RIGHT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_RIGHT, timestamp++); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + + UNLOCK + QCOMPARE(workspace()->getMovingClient(), 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..2db9fec --- /dev/null +++ b/autotests/integration/maximize_test.cpp @@ -0,0 +1,220 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "cursor.h" +#include "platform.h" +#include "shell_client.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_maximized-0"); + +class TestMaximized : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testMaximizedPassedToDeco(); + void testInitiallyMaximized(); + void testBorderlessMaximizedWindow(); +}; + +void TestMaximized::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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)); + + screens()->setCurrent(0); + KWin::Cursor::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 ShellClient gets maximized the Decoration receives the signal + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + QScopedPointer ssd(Test::waylandServerSideDecoration()->create(surface.data())); + + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QSignalSpy sizeChangedSpy(shellSurface.data(), &ShellSurface::sizeChanged); + QVERIFY(sizeChangedSpy.isValid()); + + QVERIFY(client); + QVERIFY(client->isDecorated()); + auto decoration = client->decoration(); + QVERIFY(decoration); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + + // now maximize + QVERIFY(sizeChangedSpy.isEmpty()); + QSignalSpy bordersChangedSpy(decoration, &KDecoration2::Decoration::bordersChanged); + QVERIFY(bordersChangedSpy.isValid()); + QSignalSpy maximizedChangedSpy(decoration->client().data(), &KDecoration2::DecoratedClient::maximizedChanged); + QVERIFY(maximizedChangedSpy.isValid()); + workspace()->slotWindowMaximize(); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(maximizedChangedSpy.count(), 1); + QCOMPARE(maximizedChangedSpy.last().first().toBool(), true); + QCOMPARE(bordersChangedSpy.count(), 1); + QCOMPARE(decoration->borderLeft(), 0); + QCOMPARE(decoration->borderBottom(), 0); + QCOMPARE(decoration->borderRight(), 0); + QVERIFY(decoration->borderTop() != 0); + + QVERIFY(sizeChangedSpy.isEmpty()); + QVERIFY(sizeChangedSpy.wait()); + QCOMPARE(sizeChangedSpy.count(), 1); + QCOMPARE(sizeChangedSpy.first().first().toSize(), QSize(1280, 1024 - decoration->borderTop())); + + // now unmaximize again + workspace()->slotWindowMaximize(); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(maximizedChangedSpy.count(), 2); + QCOMPARE(maximizedChangedSpy.last().first().toBool(), false); + QCOMPARE(bordersChangedSpy.count(), 2); + QVERIFY(decoration->borderTop() != 0); + QVERIFY(decoration->borderLeft() != 0); + QVERIFY(decoration->borderRight() != 0); + QVERIFY(decoration->borderBottom() != 0); + + QVERIFY(sizeChangedSpy.wait()); + QCOMPARE(sizeChangedSpy.count(), 2); + QCOMPARE(sizeChangedSpy.last().first().toSize(), QSize(100, 50)); +} + +void TestMaximized::testInitiallyMaximized() +{ + // this test verifies that a window created as maximized, will be maximized + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + + QSignalSpy sizeChangedSpy(shellSurface.data(), &ShellSurface::sizeChanged); + QVERIFY(sizeChangedSpy.isValid()); + + shellSurface->setMaximized(); + QVERIFY(sizeChangedSpy.wait()); + QCOMPARE(shellSurface->size(), QSize(1280, 1024)); + + // now let's render in an incorrect size + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QCOMPARE(client->geometry(), QRect(0, 0, 100, 50)); + QEXPECT_FAIL("", "Should go out of maximzied", Continue); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QVERIFY(client->shellSurface()->isMaximized()); +} + +void TestMaximized::testBorderlessMaximizedWindow() +{ + // test case verifies that borderless maximized window works + // see BUG 370982 + + // 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 shellSurface(Test::createShellSurface(surface.data())); + QScopedPointer ssd(Test::waylandServerSideDecoration()->create(surface.data())); + + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client->isDecorated()); + const QRect origGeo = client->geometry(); + + QSignalSpy sizeChangedSpy(shellSurface.data(), &ShellSurface::sizeChanged); + QVERIFY(sizeChangedSpy.isValid()); + + // go to maximized + shellSurface->setMaximized(); + QVERIFY(sizeChangedSpy.wait()); + QCOMPARE(shellSurface->size(), QSize(1280, 1024)); + QSignalSpy geometryChangedSpy(client, &ShellClient::geometryChanged); + QVERIFY(geometryChangedSpy.isValid()); + Test::render(surface.data(), shellSurface->size(), Qt::red); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); + QCOMPARE(client->geometry(), QRect(0, 0, 1280, 1024)); + QCOMPARE(client->isDecorated(), false); + + // go back to normal + shellSurface->setToplevel(); + QVERIFY(sizeChangedSpy.wait()); + QCOMPARE(shellSurface->size(), QSize(100, 50)); + Test::render(surface.data(), QSize(100, 50), Qt::red); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); + QCOMPARE(client->geometry(), origGeo); + QCOMPARE(client->isDecorated(), true); +} + +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..00dc2d7 --- /dev/null +++ b/autotests/integration/modifier_only_shortcut_test.cpp @@ -0,0 +1,386 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(); + virtual ~Target(); + +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 workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void ModifierOnlyShortcutTest::init() +{ + screens()->setCurrent(0); + KWin::Cursor::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..3c3b350 --- /dev/null +++ b/autotests/integration/move_resize_window_test.cpp @@ -0,0 +1,857 @@ + +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "atoms.h" +#include "platform.h" +#include "abstract_client.h" +#include "client.h" +#include "cursor.h" +#include "effects.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.h" + +#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 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_data(); + void testClientSideMove(); + void testPlasmaShellSurfaceMovable_data(); + void testPlasmaShellSurfaceMovable(); + void testNetMove(); + void testAdjustClientGeometryOfAutohidingX11Panel_data(); + void testAdjustClientGeometryOfAutohidingX11Panel(); + void testAdjustClientGeometryOfAutohidingWaylandPanel_data(); + void testAdjustClientGeometryOfAutohidingWaylandPanel(); + +private: + KWayland::Client::ConnectionThread *m_connection = nullptr; + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::Shell *m_shell = nullptr; +}; + +void MoveResizeWindowTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType("MaximizeMode"); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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(); + m_shell = Test::waylandShell(); + + 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::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &ShellSurface::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->geometry(), QRect(0, 0, 100, 50)); + QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); + QVERIFY(geometryChangedSpy.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()); + + // 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()->getMovingClient() == nullptr); + QCOMPARE(c->isMove(), false); + workspace()->slotWindowMove(); + QCOMPARE(workspace()->getMovingClient(), 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 = Cursor::pos(); + c->keyPressEvent(Qt::Key_Right); + c->updateMoveResize(Cursor::pos()); + QCOMPARE(Cursor::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(Cursor::pos()); + QCOMPARE(Cursor::pos(), cursorPos + QPoint(16, 0)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + QCOMPARE(windowStepUserMovedResizedSpy.count(), 1); + + c->keyPressEvent(Qt::Key_Down | Qt::ALT); + c->updateMoveResize(Cursor::pos()); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); + QCOMPARE(windowStepUserMovedResizedSpy.count(), 2); + QCOMPARE(c->geometry(), QRect(16, 32, 100, 50)); + QCOMPARE(Cursor::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->geometry(), QRect(16, 32, 100, 50)); + QCOMPARE(c->isMove(), false); + QVERIFY(workspace()->getMovingClient() == 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::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &ShellSurface::sizeChanged); + QVERIFY(sizeChangeSpy.isValid()); + // let's render + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QSignalSpy surfaceSizeChangedSpy(shellSurface.data(), &ShellSurface::sizeChanged); + QVERIFY(surfaceSizeChangedSpy.isValid()); + + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->geometry(), QRect(0, 0, 100, 50)); + QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); + QVERIFY(geometryChangedSpy.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 + QVERIFY(workspace()->getMovingClient() == nullptr); + QCOMPARE(c->isMove(), false); + QCOMPARE(c->isResize(), false); + workspace()->slotWindowResize(); + QCOMPARE(workspace()->getMovingClient(), c); + QCOMPARE(startMoveResizedSpy.count(), 1); + QCOMPARE(moveResizedChangedSpy.count(), 1); + QCOMPARE(c->isResize(), true); + QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); + + // trigger a change + const QPoint cursorPos = Cursor::pos(); + c->keyPressEvent(Qt::Key_Right); + c->updateMoveResize(Cursor::pos()); + QCOMPARE(Cursor::pos(), cursorPos + QPoint(8, 0)); + // should result in a size change request + QVERIFY(surfaceSizeChangedSpy.wait()); + QCOMPARE(surfaceSizeChangedSpy.count(), 1); + QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(108, 50)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); + // now render new size + Test::render(surface.data(), QSize(108, 50), Qt::blue); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(c->geometry(), QRect(0, 0, 108, 50)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); + + // go down + c->keyPressEvent(Qt::Key_Down); + c->updateMoveResize(Cursor::pos()); + QCOMPARE(Cursor::pos(), cursorPos + QPoint(8, 8)); + QVERIFY(surfaceSizeChangedSpy.wait()); + QCOMPARE(surfaceSizeChangedSpy.count(), 2); + QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(108, 58)); + // now render new size + Test::render(surface.data(), QSize(108, 58), Qt::blue); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(c->geometry(), QRect(0, 0, 108, 58)); + QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); + + // let's end + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); + c->keyPressEvent(Qt::Key_Enter); + QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); + QCOMPARE(moveResizedChangedSpy.count(), 2); + QCOMPARE(c->isResize(), false); + QVERIFY(workspace()->getMovingClient() == nullptr); + surface.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::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &ShellSurface::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->geometry(), QRect(0, 0, 100, 50)); + + // let's place it centered + Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); + QCOMPARE(c->geometry(), QRect(590, 487, 100, 50)); + + QFETCH(QString, methodCall); + QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); + QTEST(c->geometry(), "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::createShellSurface(surface1.data())); + QVERIFY(!shellSurface1.isNull()); + QScopedPointer shellSurface2(Test::createShellSurface(surface2.data())); + QVERIFY(!shellSurface2.isNull()); + QScopedPointer shellSurface3(Test::createShellSurface(surface3.data())); + QVERIFY(!shellSurface3.isNull()); + QScopedPointer shellSurface4(Test::createShellSurface(surface4.data())); + QVERIFY(!shellSurface4.isNull()); + auto renderWindow = [this] (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->geometry().size(), QSize(10, 10)); + // let's place it centered + Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); + QCOMPARE(c->geometry(), QRect(635, 507, 10, 10)); + QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); + QCOMPARE(c->geometry(), 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::createShellSurface(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->geometry(), QRect(590, 487, 100, 50)); + + QFETCH(QString, methodCall); + QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); + QTEST(c->geometry(), "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::createShellSurface(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::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &ShellSurface::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->geometry(), 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 geometryChangedSpy(c, &AbstractClient::geometryChanged); + QVERIFY(geometryChangedSpy.isValid()); + m_connection->flush(); + QVERIFY(geometryChangedSpy.wait()); + QTEST(c->geometry(), "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::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &ShellSurface::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_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void MoveResizeWindowTest::testClientSideMove() +{ + using namespace KWayland::Client; + Cursor::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()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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->geometry(); + Cursor::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()); + if (auto s = qobject_cast(shellSurface.data())) { + s->requestMove(Test::waylandSeat(), buttonSpy.first().first().value()); + } else if (auto s = qobject_cast(shellSurface.data())) { + s->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->geometry(), 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::createShellSurface(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()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + const QRect origGeo = client->geometry(); + + // let's move the cursor outside the window + Cursor::setPos(screens()->geometry(0).center()); + QVERIFY(!origGeo.contains(Cursor::pos())); + + QSignalSpy moveStartSpy(client, &Client::clientStartUserMovedResized); + QVERIFY(moveStartSpy.isValid()); + QSignalSpy moveEndSpy(client, &Client::clientFinishUserMovedResized); + QVERIFY(moveEndSpy.isValid()); + QSignalSpy moveStepSpy(client, &Client::clientStepUserMovedResized); + QVERIFY(moveStepSpy.isValid()); + QVERIFY(!workspace()->getMovingClient()); + + // 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()->getMovingClient(), client); + QVERIFY(client->isMove()); + QCOMPARE(client->geometryRestore(), origGeo); + QCOMPARE(Cursor::pos(), origGeo.center()); + + // let's move a step + Cursor::setPos(Cursor::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->geometry().center().x(), client->geometry().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, &Client::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()); + Client *panel = windowCreatedSpy.first().first().value(); + QVERIFY(panel); + QCOMPARE(panel->window(), w); + QCOMPARE(panel->geometry(), panelGeometry); + QVERIFY(panel->isDock()); + + // let's create a window + using namespace KWayland::Client; + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createShellSurface(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, &Client::windowClosed); + QVERIFY(panelClosedSpy.isValid()); + QVERIFY(panelClosedSpy.wait()); + + // snap once more + QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); + + // and close + QSignalSpy windowClosedSpy(testWindow, &ShellClient::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::createShellSurface(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->geometry(), panelGeometry); + QVERIFY(panel->isDock()); + + // let's create a window + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createShellSurface(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, &ShellClient::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, &ShellClient::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + shellSurface.reset(); + surface.reset(); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::MoveResizeWindowTest) +#include "move_resize_window_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..98eef83 --- /dev/null +++ b/autotests/integration/no_xdg_runtime_dir_test.cpp @@ -0,0 +1,47 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 fials to init + QVERIFY(!waylandServer()->init(s_socketName.toLocal8Bit())); +} + +WAYLANDTEST_MAIN(NoXdgRuntimeDirTest) +#include "no_xdg_runtime_dir_test.moc" diff --git a/autotests/integration/plasma_surface_test.cpp b/autotests/integration/plasma_surface_test.cpp new file mode 100644 index 0000000..41a9665 --- /dev/null +++ b/autotests/integration/plasma_surface_test.cpp @@ -0,0 +1,446 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "cursor.h" +#include "shell_client.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; + +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_data(); + void testOSDPlacementManualPosition(); + void testPanelTypeHasStrut_data(); + void testPanelTypeHasStrut(); + void testPanelActivate_data(); + void testPanelActivate(); + +private: + KWayland::Client::Compositor *m_compositor = nullptr; + Shell *m_shell = nullptr; + PlasmaShell *m_plasmaShell = nullptr; +}; + +void PlasmaSurfaceTest::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); +} + +void PlasmaSurfaceTest::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::PlasmaShell)); + m_compositor = Test::waylandCompositor(); + m_shell = Test::waylandShell(); + m_plasmaShell = Test::waylandPlasmaShell(); + + KWin::Cursor::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; +} + +void PlasmaSurfaceTest::testRoleOnAllDesktops() +{ + // this test verifies that a ShellClient is set on all desktops when the role changes + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createShellSurface(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::createShellSurface(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; +} + +void PlasmaSurfaceTest::testAcceptsFocus() +{ + // this test verifies that some surface roles don't get focus + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QScopedPointer shellSurface(Test::createShellSurface(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::createShellSurface(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::createShellSurface(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->geometry(), QRect(590, 649, 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->geometry(), QRect(590, 649, 100, 50)); + + // change size of window + QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryShapeChanged); + QVERIFY(geometryChangedSpy.isValid()); + Test::render(surface.data(), QSize(200, 100), Qt::red); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(c->geometry(), QRect(540, 616, 200, 100)); +} + +void PlasmaSurfaceTest::testOSDPlacementManualPosition_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wl-shell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgv5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgv6") << Test::ShellSurfaceType::XdgShellV6; +} + +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)); + + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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->geometry(), QRect(50, 70, 100, 50)); +} + + +void PlasmaSurfaceTest::testPanelTypeHasStrut_data() +{ + QTest::addColumn("type"); + QTest::addColumn("panelBehavior"); + QTest::addColumn("expectedStrut"); + QTest::addColumn("expectedMaxArea"); + QTest::addColumn("expectedLayer"); + + QTest::newRow("always visible - wlShell") << Test::ShellSurfaceType::WlShell << PlasmaShellSurface::PanelBehavior::AlwaysVisible << true << QRect(0, 50, 1280, 974) << KWin::DockLayer; + QTest::newRow("always visible - xdgShellV5") << Test::ShellSurfaceType::XdgShellV5 << PlasmaShellSurface::PanelBehavior::AlwaysVisible << true << QRect(0, 50, 1280, 974) << KWin::DockLayer; + QTest::newRow("always visible - xdgShellV6") << Test::ShellSurfaceType::XdgShellV6 << PlasmaShellSurface::PanelBehavior::AlwaysVisible << true << QRect(0, 50, 1280, 974) << KWin::DockLayer; + QTest::newRow("autohide - wlShell") << Test::ShellSurfaceType::WlShell << PlasmaShellSurface::PanelBehavior::AutoHide << false << QRect(0, 0, 1280, 1024) << KWin::AboveLayer; + QTest::newRow("autohide - xdgShellV5") << Test::ShellSurfaceType::XdgShellV5 << PlasmaShellSurface::PanelBehavior::AutoHide << false << QRect(0, 0, 1280, 1024) << KWin::AboveLayer; + QTest::newRow("autohide - xdgShellV6") << Test::ShellSurfaceType::XdgShellV6 << PlasmaShellSurface::PanelBehavior::AutoHide << false << QRect(0, 0, 1280, 1024) << KWin::AboveLayer; + QTest::newRow("windows can cover - wlShell") << Test::ShellSurfaceType::WlShell << PlasmaShellSurface::PanelBehavior::WindowsCanCover << false << QRect(0, 0, 1280, 1024) << KWin::NormalLayer; + QTest::newRow("windows can cover - xdgShellV5") << Test::ShellSurfaceType::XdgShellV5 << PlasmaShellSurface::PanelBehavior::WindowsCanCover << false << QRect(0, 0, 1280, 1024) << KWin::NormalLayer; + QTest::newRow("windows can cover - xdgShellV6") << Test::ShellSurfaceType::XdgShellV6 << PlasmaShellSurface::PanelBehavior::WindowsCanCover << false << QRect(0, 0, 1280, 1024) << KWin::NormalLayer; + QTest::newRow("windows go below - wlShell") << Test::ShellSurfaceType::WlShell << PlasmaShellSurface::PanelBehavior::WindowsGoBelow << false << QRect(0, 0, 1280, 1024) << KWin::DockLayer; + QTest::newRow("windows go below - xdgShellV5") << Test::ShellSurfaceType::XdgShellV5 << PlasmaShellSurface::PanelBehavior::WindowsGoBelow << false << QRect(0, 0, 1280, 1024) << KWin::DockLayer; + QTest::newRow("windows go below - xdgShellV6") << Test::ShellSurfaceType::XdgShellV6 << PlasmaShellSurface::PanelBehavior::WindowsGoBelow << false << QRect(0, 0, 1280, 1024) << KWin::DockLayer; +} + +void PlasmaSurfaceTest::testPanelTypeHasStrut() +{ + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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->geometry(), 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::createShellSurface(Test::ShellSurfaceType::WlShell, 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->geometry(), 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::createShellSurface(Test::ShellSurfaceType::WlShell, 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->geometry(), 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::Cursor::setPos(triggerPoint); + 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::createShellSurface(Test::ShellSurfaceType::WlShell, 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..3cc9273 --- /dev/null +++ b/autotests/integration/plasmawindow_test.cpp @@ -0,0 +1,367 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "client.h" +#include "cursor.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.h" +#include + +#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; + Shell *m_shell = nullptr; +}; + +void PlasmaWindowTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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(); + m_shell = Test::waylandShell(); + + screens()->setCurrent(0); + Cursor::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()); + Client *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->geometry()); + 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->geometry(); + QVERIFY(geoBeforeShade.isValid()); + QVERIFY(!geoBeforeShade.isEmpty()); + workspace()->slotWindowShade(); + QVERIFY(client->isShade()); + QVERIFY(client->geometry() != geoBeforeShade); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(pw->geometry(), client->geometry()); + // and unshade again + workspace()->slotWindowShade(); + QVERIFY(!client->isShade()); + QCOMPARE(client->geometry(), 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, &Client::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(); + +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::createShellSurface(parentSurface.data())); + // map that window + Test::render(parentSurface.data(), QSize(100, 50), Qt::blue); + // this should create a plasma window + QVERIFY(plasmaWindowCreatedSpy.wait()); + + // now let's create a popup window for it + QScopedPointer popupSurface(Test::createSurface()); + QScopedPointer popupShellSurface(Test::createShellSurface(popupSurface.data())); + popupShellSurface->setTransient(parentSurface.data(), QPoint(0, 0), ShellSurface::TransientFlag::NoFocus); + // let's map it + Test::render(popupSurface.data(), QSize(100, 50), Qt::blue); + + // this should not create a plasma window + QVERIFY(!plasmaWindowCreatedSpy.wait()); + + // now the same with an already mapped surface when we create the shell surface + QScopedPointer popup2Surface(Test::createSurface()); + Test::render(popup2Surface.data(), QSize(100, 50), Qt::blue); + QScopedPointer popup2ShellSurface(Test::createShellSurface(popup2Surface.data())); + popup2ShellSurface->setTransient(popupSurface.data(), QPoint(0, 0), ShellSurface::TransientFlag::NoFocus); + + // this should not create a plasma window + QEXPECT_FAIL("", "The call to setTransient comes to late the window is already mapped then", Continue); + QVERIFY(!plasmaWindowCreatedSpy.wait()); + + // let's destroy the windows + QCOMPARE(waylandServer()->clients().count(), 3); + QSignalSpy destroyed1Spy(waylandServer()->clients().last(), &QObject::destroyed); + QVERIFY(destroyed1Spy.isValid()); + popup2Surface->attachBuffer(Buffer::Ptr()); + popup2Surface->commit(Surface::CommitFlag::None); + popup2ShellSurface.reset(); + popup2Surface.reset(); + QVERIFY(destroyed1Spy.wait()); + QCOMPARE(waylandServer()->clients().count(), 2); + QSignalSpy destroyed2Spy(waylandServer()->clients().last(), &QObject::destroyed); + QVERIFY(destroyed2Spy.isValid()); + popupSurface->attachBuffer(Buffer::Ptr()); + popupSurface->commit(Surface::CommitFlag::None); + popupShellSurface.reset(); + popupSurface.reset(); + QVERIFY(destroyed2Spy.wait()); + QCOMPARE(waylandServer()->clients().count(), 1); + QSignalSpy destroyed3Spy(waylandServer()->clients().last(), &QObject::destroyed); + QVERIFY(destroyed3Spy.isValid()); + parentSurface->attachBuffer(Buffer::Ptr()); + parentSurface->commit(Surface::CommitFlag::None); + parentShellSurface.reset(); + parentSurface.reset(); + QVERIFY(destroyed3Spy.wait()); +} + +void PlasmaWindowTest::testLockScreenNoPlasmaWindow() +{ + if (!QFile::exists(QStringLiteral("/dev/dri/card0"))) { + QSKIP("Needs a dri device"); + } + // 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 ShellClient as it'a a little bit more complex setup + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + 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::createShellSurface(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..3d0ff45 --- /dev/null +++ b/autotests/integration/platformcursor.cpp @@ -0,0 +1,71 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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(Cursor::pos(), QPoint(639, 511)); + QCOMPARE(QCursor::pos(), QPoint(639, 511)); + + // let's set the pos through QCursor API + QCursor::setPos(QPoint(10, 10)); + QCOMPARE(Cursor::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(Cursor::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..8e2e568 --- /dev/null +++ b/autotests/integration/pointer_constraints_test.cpp @@ -0,0 +1,384 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "cursor.h" +#include "keyboard_input.h" +#include "platform.h" +#include "pointer_input.h" +#include "shell_client.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#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_data(); + void testLockedPointer(); + void testCloseWindowWithLockedPointer_data(); + void testCloseWindowWithLockedPointer(); +}; + +void TestPointerConstraints::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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(workspaceCreatedSpy.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::Cursor::setPos(QPoint(1280, 512)); +} + +void TestPointerConstraints::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestPointerConstraints::testConfinedPointer_data() +{ + QTest::addColumn("type"); + 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("wlShell - bottomLeft") << Test::ShellSurfaceType::WlShell << bottomLeft << -1 << 1; + QTest::newRow("wlShell - bottomRight") << Test::ShellSurfaceType::WlShell << bottomRight << 1 << 1; + QTest::newRow("wlShell - topLeft") << Test::ShellSurfaceType::WlShell << topLeft << -1 << -1; + QTest::newRow("wlShell - topRight") << Test::ShellSurfaceType::WlShell << topRight << 1 << -1; + QTest::newRow("XdgShellV5 - bottomLeft") << Test::ShellSurfaceType::XdgShellV5 << bottomLeft << -1 << 1; + QTest::newRow("XdgShellV5 - bottomRight") << Test::ShellSurfaceType::XdgShellV5 << bottomRight << 1 << 1; + QTest::newRow("XdgShellV5 - topLeft") << Test::ShellSurfaceType::XdgShellV5 << topLeft << -1 << -1; + QTest::newRow("XdgShellV5 - topRight") << Test::ShellSurfaceType::XdgShellV5 << topRight << 1 << -1; + QTest::newRow("XdgShellV6 - bottomLeft") << Test::ShellSurfaceType::XdgShellV6 << bottomLeft << -1 << 1; + QTest::newRow("XdgShellV6 - bottomRight") << Test::ShellSurfaceType::XdgShellV6 << bottomRight << 1 << 1; + QTest::newRow("XdgShellV6 - topLeft") << Test::ShellSurfaceType::XdgShellV6 << topLeft << -1 << -1; + QTest::newRow("XdgShellV6 - topRight") << Test::ShellSurfaceType::XdgShellV6 << 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()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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->geometry().topLeft() == QPoint(0, 0)) { + c->move(QPoint(1, 1)); + } + QVERIFY(!c->geometry().contains(KWin::Cursor::pos())); + + // now let's confine + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::Cursor::setPos(c->geometry().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::Cursor::setPos(QPoint(1280, 512)); + QVERIFY(pointerPositionChangedSpy.isEmpty()); + QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); + + // TODO: test relative motion + QFETCH(PointerFunc, positionFunction); + const QPoint position = positionFunction(c->geometry()); + KWin::Cursor::setPos(position); + QCOMPARE(pointerPositionChangedSpy.count(), 1); + QCOMPARE(KWin::Cursor::pos(), position); + // moving one to right should not be possible + QFETCH(int, xOffset); + KWin::Cursor::setPos(position + QPoint(xOffset, 0)); + QCOMPARE(pointerPositionChangedSpy.count(), 1); + QCOMPARE(KWin::Cursor::pos(), position); + // moving one to bottom should not be possible + QFETCH(int, yOffset); + KWin::Cursor::setPos(position + QPoint(0, yOffset)); + QCOMPARE(pointerPositionChangedSpy.count(), 1); + QCOMPARE(KWin::Cursor::pos(), position); + + // 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()->window().data())); + 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()->window().data())); + 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::createShellSurface(type, 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()->window()->surface(), &KWayland::Server::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_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +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()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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->geometry().contains(KWin::Cursor::pos())); + + // now let's lock + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::Cursor::setPos(c->geometry().center()); + QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); + QCOMPARE(input()->pointer()->isConstrained(), true); + QVERIFY(lockedSpy.wait()); + + // try to move the pointer + // TODO: add relative pointer + KWin::Cursor::setPos(c->geometry().center() + QPoint(1, 1)); + QCOMPARE(KWin::Cursor::pos(), c->geometry().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::Cursor::setPos(c->geometry().center() + QPoint(1, 1)); + QCOMPARE(KWin::Cursor::pos(), c->geometry().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()->window().data())); + QVERIFY(lockedSpy2.wait()); + QCOMPARE(input()->pointer()->isConstrained(), true); + + // try to move the pointer + QCOMPARE(input()->pointer()->isConstrained(), true); + KWin::Cursor::setPos(c->geometry().center()); + QCOMPARE(KWin::Cursor::pos(), c->geometry().center() + QPoint(1, 1)); + + // delete pointer lock + lockedPointer.reset(nullptr); + Test::flushWaylandConnection(); + + QSignalSpy constraintsChangedSpy(input()->pointer()->window()->surface(), &KWayland::Server::SurfaceInterface::pointerConstraintsChanged); + QVERIFY(constraintsChangedSpy.isValid()); + QVERIFY(constraintsChangedSpy.wait()); + + // moving cursor should be allowed again + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::Cursor::setPos(c->geometry().center()); + QCOMPARE(KWin::Cursor::pos(), c->geometry().center()); +} + +void TestPointerConstraints::testCloseWindowWithLockedPointer_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("XdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("XdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void TestPointerConstraints::testCloseWindowWithLockedPointer() +{ + // test case which verifies that the pointer gets unlocked when the window for it gets closed + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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->geometry().contains(KWin::Cursor::pos())); + + // now let's lock + QCOMPARE(input()->pointer()->isConstrained(), false); + KWin::Cursor::setPos(c->geometry().center()); + QCOMPARE(KWin::Cursor::pos(), c->geometry().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..db5041d --- /dev/null +++ b/autotests/integration/pointer_input.cpp @@ -0,0 +1,1304 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 "shell_client.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace KWin +{ + +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 testModifierScrollOpacity_data(); + void testModifierScrollOpacity(); + 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(); + +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; + KWayland::Client::Shell *m_shell = nullptr; +}; + +void PointerInputTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + 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(workspaceCreatedSpy.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_shell = Test::waylandShell(); + m_seat = Test::waylandSeat(); + + screens()->setCurrent(0); + Cursor::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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + ShellSurface *shellSurface = Test::createShellSurface(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 + Cursor::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 + Cursor::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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + ShellSurface *shellSurface = Test::createShellSurface(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 + Cursor::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 + Cursor::setPos(10, 10); + + // create a window + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + ShellSurface *shellSurface = Test::createShellSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + + QCOMPARE(window->pos(), QPoint(0, 0)); + QVERIFY(window->geometry().contains(Cursor::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(Cursor::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 + Cursor::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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + ShellSurface *shellSurface = Test::createShellSurface(surface, surface); + QVERIFY(shellSurface); + render(surface, QSize(1280, 1024)); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + QVERIFY(!window->geometry().contains(Cursor::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(Cursor::pos(), QPoint(639, 511)); + QVERIFY(window->geometry().contains(Cursor::pos())); + + // and we should get an enter event + QVERIFY(enteredSpy.wait()); + QCOMPARE(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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + ShellSurface *shellSurface = Test::createShellSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + + // move cursor on window + Cursor::setPos(window->geometry().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::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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + ShellSurface *shellSurface = Test::createShellSurface(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 + Cursor::setPos(window->geometry().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::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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface1 = Test::createSurface(m_compositor); + QVERIFY(surface1); + ShellSurface *shellSurface1 = Test::createShellSurface(surface1, surface1); + QVERIFY(shellSurface1); + render(surface1); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window1 = workspace()->activeClient(); + QVERIFY(window1); + Surface *surface2 = Test::createSurface(m_compositor); + QVERIFY(surface2); + ShellSurface *shellSurface2 = Test::createShellSurface(surface2, surface2); + QVERIFY(shellSurface2); + render(surface2); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window2 = workspace()->activeClient(); + QVERIFY(window2); + QVERIFY(window1 != window2); + + // move cursor to the inactive window + Cursor::setPos(window1->geometry().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 + Cursor::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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface1 = Test::createSurface(m_compositor); + QVERIFY(surface1); + ShellSurface *shellSurface1 = Test::createShellSurface(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); + ShellSurface *shellSurface2 = Test::createShellSurface(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->geometry().intersects(window2->geometry())); + + // 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->geometry().contains(10, 10)); + QVERIFY(!window2->geometry().contains(10, 10)); + Cursor::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 + Cursor::setPos(810, 810); + QVERIFY(stackingOrderChangedSpy.wait()); + QCOMPARE(stackingOrderChangedSpy.count(), 2); + QCOMPARE(workspace()->topClientOnDesktop(1, -1), window2); + Cursor::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 + Cursor::setPos(810, 810); + Cursor::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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface1 = Test::createSurface(m_compositor); + QVERIFY(surface1); + ShellSurface *shellSurface1 = Test::createShellSurface(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); + ShellSurface *shellSurface2 = Test::createShellSurface(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->geometry().intersects(window2->geometry())); + + // 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->geometry().contains(10, 10)); + QVERIFY(!window2->geometry().contains(10, 10)); + Cursor::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(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface1 = Test::createSurface(m_compositor); + QVERIFY(surface1); + ShellSurface *shellSurface1 = Test::createShellSurface(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); + ShellSurface *shellSurface2 = Test::createShellSurface(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->geometry().intersects(window2->geometry())); + // 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->geometry().contains(900, 900)); + QVERIFY(window2->geometry().contains(900, 900)); + Cursor::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 + Cursor::setPos(800, 800); + auto p = input()->pointer(); + // at the moment it should be the fallback cursor + const QImage fallbackCursor = p->cursorImage(); + QVERIFY(!fallbackCursor.isNull()); + + // create a window + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + ShellSurface *shellSurface = Test::createShellSurface(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->geometry().center()); + QCOMPARE(p->window().data(), window); + QCOMPARE(p->cursorImage(), 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(p->cursorImage(), red); + QCOMPARE(p->cursorHotSpot(), QPoint(5, 5)); + // change hotspot + pointer->setCursor(cursorSurface, QPoint(6, 6)); + Test::flushWaylandConnection(); + QTRY_COMPARE(p->cursorHotSpot(), QPoint(6, 6)); + QCOMPARE(p->cursorImage(), 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(p->cursorImage(), blue); + QCOMPARE(p->cursorHotSpot(), QPoint(6, 6)); + + // scaled cursor + QImage blueScaled = QImage(QSize(20, 20), QImage::Format_ARGB32_Premultiplied); + 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(p->cursorImage(), blueScaled); + QCOMPARE(p->cursorImage().devicePixelRatio(), 2.0); + QCOMPARE(p->cursorHotSpot(), QPoint(6, 6)); //surface-local (so not changed) + + // hide the cursor + pointer->setCursor(nullptr); + Test::flushWaylandConnection(); + QTRY_VERIFY(p->cursorImage().isNull()); + + // move cursor somewhere else, should reset to fallback cursor + Cursor::setPos(window->geometry().bottomLeft() + QPoint(20, 20)); + QVERIFY(p->window().isNull()); + QVERIFY(!p->cursorImage().isNull()); + QCOMPARE(p->cursorImage(), fallbackCursor); +} + +class HelperEffect : public Effect +{ + Q_OBJECT +public: + HelperEffect() {} + ~HelperEffect() {} +}; + +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); + 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); + auto p = input()->pointer(); + // here we should have the fallback cursor + const QImage fallback = p->cursorImage(); + QVERIFY(!fallback.isNull()); + + // now let's create a window + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + ShellSurface *shellSurface = Test::createShellSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + + // and move cursor to the window + QVERIFY(!window->geometry().contains(QPoint(800, 800))); + Cursor::setPos(window->geometry().center()); + QVERIFY(enteredSpy.wait()); + // cursor image should still be fallback + QCOMPARE(p->cursorImage(), fallback); + + // now create an effect and set an override cursor + QScopedPointer effect(new HelperEffect); + effects->startMouseInterception(effect.data(), Qt::SizeAllCursor); + const QImage sizeAll = p->cursorImage(); + 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(p->cursorImage(), fallback); + + // back to size all + effects->defineCursor(Qt::SizeAllCursor); + QCOMPARE(p->cursorImage(), sizeAll); + + // move cursor outside the window area + Cursor::setPos(800, 800); + // and end the override, which should switch to fallback + effects->stopMouseInterception(effect.data()); + QCOMPARE(p->cursorImage(), fallback); + + // start mouse interception again + effects->startMouseInterception(effect.data(), Qt::SizeAllCursor); + QCOMPARE(p->cursorImage(), sizeAll); + + // move cursor to area of window + Cursor::setPos(window->geometry().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(p->cursorImage().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()); + + Cursor::setPos(800, 800); + + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + ShellSurface *shellSurface = Test::createShellSurface(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->geometry().contains(QPoint(800, 800))); + Cursor::setPos(window->geometry().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 + Surface *popupSurface = Test::createSurface(m_compositor); + QVERIFY(popupSurface); + ShellSurface *popupShellSurface = Test::createShellSurface(popupSurface, popupSurface); + QVERIFY(popupShellSurface); + QSignalSpy popupDoneSpy(popupShellSurface, &ShellSurface::popupDone); + QVERIFY(popupDoneSpy.isValid()); + // TODO: proper serial + popupShellSurface->setTransientPopup(surface, m_seat, 0, QPoint(80, 20)); + render(popupSurface); + 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 + Cursor::setPos(popupClient->geometry().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 + Cursor::setPos(popupClient->geometry().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()); + + Cursor::setPos(800, 800); + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + ShellSurface *shellSurface = Test::createShellSurface(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->geometry().contains(QPoint(800, 800))); + Cursor::setPos(window->geometry().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 + Surface *popupSurface = Test::createSurface(m_compositor); + QVERIFY(popupSurface); + ShellSurface *popupShellSurface = Test::createShellSurface(popupSurface, popupSurface); + QVERIFY(popupShellSurface); + QSignalSpy popupDoneSpy(popupShellSurface, &ShellSurface::popupDone); + QVERIFY(popupDoneSpy.isValid()); + // TODO: proper serial + popupShellSurface->setTransientPopup(surface, m_seat, 0, QPoint(80, 20)); + render(popupSurface); + 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 + Cursor::setPos(window->geometry().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()); + + Cursor::setPos(800, 800); + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Surface *surface = Test::createSurface(m_compositor); + QVERIFY(surface); + ShellSurface *shellSurface = Test::createShellSurface(surface, surface); + QVERIFY(shellSurface); + render(surface); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *window = workspace()->activeClient(); + QVERIFY(window); + + // move cursor over window + QVERIFY(!window->geometry().contains(QPoint(800, 800))); + Cursor::setPos(window->geometry().center()); + QVERIFY(enteredSpy.wait()); + // click inside window + quint32 timestamp = 0; + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + + // now create a second window as transient + Surface *popupSurface = Test::createSurface(m_compositor); + QVERIFY(popupSurface); + ShellSurface *popupShellSurface = Test::createShellSurface(popupSurface, popupSurface); + QVERIFY(popupShellSurface); + popupShellSurface->setTransient(surface, QPoint(0, 0)); + render(popupSurface); + QVERIFY(clientAddedSpy.wait()); + auto popupClient = clientAddedSpy.last().first().value(); + QVERIFY(popupClient); + QVERIFY(popupClient != window); + QCOMPARE(window->geometry(), popupClient->geometry()); + 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); + Cursor::setPos(startPos); + QCOMPARE(Cursor::pos(), startPos); + + // perform movement + QFETCH(QPoint, targetPos); + kwinApp()->platform()->pointerMotion(targetPos, 1); + + QFETCH(QPoint, expectedPos); + QCOMPARE(Cursor::pos(), expectedPos); +} + +} + +WAYLANDTEST_MAIN(KWin::PointerInputTest) +#include "pointer_input.moc" diff --git a/autotests/integration/quick_tiling_test.cpp b/autotests/integration/quick_tiling_test.cpp new file mode 100644 index 0000000..f72e6ed --- /dev/null +++ b/autotests/integration/quick_tiling_test.cpp @@ -0,0 +1,904 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "abstract_client.h" +#include "client.h" +#include "cursor.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.h" +#include "scripting/scripting.h" + +#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 testQuickTilingPointerMoveXdgShell_data(); + void testQuickTilingPointerMoveXdgShell(); + void testQuickTilingTouchMoveXdgShell_data(); + void testQuickTilingTouchMoveXdgShell(); + 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; + KWayland::Client::Shell *m_shell = nullptr; +}; + +void QuickTilingTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType("MaximizeMode"); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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(workspaceCreatedSpy.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(); + m_shell = Test::waylandShell(); + + 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::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &ShellSurface::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->geometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); + QVERIFY(geometryChangedSpy.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->geometry(), QRect(0, 0, 100, 50)); + // but quick tile mode already changed + QCOMPARE(c->quickTileMode(), mode); + + // but we got requested a new geometry + QVERIFY(sizeChangeSpy.wait()); + QCOMPARE(sizeChangeSpy.count(), 1); + QCOMPARE(sizeChangeSpy.first().first().toSize(), expectedGeometry.size()); + + // attach a new image + Test::render(surface.data(), expectedGeometry.size(), Qt::red); + m_connection->flush(); + + QVERIFY(geometryChangedSpy.wait()); + QEXPECT_FAIL("maximize", "Geometry changed called twice for maximize", Continue); + QCOMPARE(geometryChangedSpy.count(), 1); + QCOMPARE(c->geometry(), 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->geometry(), "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::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &ShellSurface::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->geometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(c->maximizeMode(), MaximizeRestore); + QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); + QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy maximizeChangedSpy1(c, SIGNAL(clientMaximizedStateChanged(KWin::AbstractClient*,MaximizeMode))); + QVERIFY(maximizeChangedSpy1.isValid()); + QSignalSpy maximizeChangedSpy2(c, SIGNAL(clientMaximizedStateChanged(KWin::AbstractClient*,bool,bool))); + QVERIFY(maximizeChangedSpy2.isValid()); + + c->setQuickTileMode(QuickTileFlag::Maximize, true); + QCOMPARE(quickTileChangedSpy.count(), 1); + 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); + // at this point the geometry did not yet change + QCOMPARE(c->geometry(), QRect(0, 0, 100, 50)); + // but quick tile mode already changed + QCOMPARE(c->quickTileMode(), QuickTileFlag::Maximize); + QCOMPARE(c->maximizeMode(), MaximizeFull); + QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); + + // but we got requested a new geometry + QVERIFY(sizeChangeSpy.wait()); + QCOMPARE(sizeChangeSpy.count(), 1); + QCOMPARE(sizeChangeSpy.first().first().toSize(), QSize(1280, 1024)); + + // attach a new image + Test::render(surface.data(), QSize(1280, 1024), Qt::red); + m_connection->flush(); + + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(geometryChangedSpy.count(), 2); + QCOMPARE(c->geometry(), QRect(0, 0, 1280, 1024)); + QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); + + // go back to quick tile none + QFETCH(QuickTileMode, mode); + c->setQuickTileMode(mode, true); + QCOMPARE(quickTileChangedSpy.count(), 2); + 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); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(c->maximizeMode(), MaximizeRestore); + // geometry not yet changed + QCOMPARE(c->geometry(), QRect(0, 0, 1280, 1024)); + QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); + // we got requested a new geometry + QVERIFY(sizeChangeSpy.wait()); + QCOMPARE(sizeChangeSpy.count(), 2); + QCOMPARE(sizeChangeSpy.last().first().toSize(), QSize(100, 50)); + + // render again + Test::render(surface.data(), QSize(100, 50), Qt::yellow); + m_connection->flush(); + + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(geometryChangedSpy.count(), 4); + QCOMPARE(c->geometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); +} + +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::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &ShellSurface::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->geometry(), 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()->getMovingClient()); + QCOMPARE(Cursor::pos(), QPoint(49, 24)); + + QFETCH(QPoint, targetPos); + quint32 timestamp = 1; + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); + while (Cursor::pos().x() > targetPos.x()) { + kwinApp()->platform()->keyboardKeyPressed(KEY_LEFT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_LEFT, timestamp++); + } + while (Cursor::pos().x() < targetPos.x()) { + kwinApp()->platform()->keyboardKeyPressed(KEY_RIGHT, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_RIGHT, timestamp++); + } + while (Cursor::pos().y() < targetPos.y()) { + kwinApp()->platform()->keyboardKeyPressed(KEY_DOWN, timestamp++); + kwinApp()->platform()->keyboardKeyReleased(KEY_DOWN, timestamp++); + } + while (Cursor::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(Cursor::pos(), targetPos); + QVERIFY(!workspace()->getMovingClient()); + + 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::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &ShellSurface::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->geometry(), 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()->getMovingClient()); + QCOMPARE(Cursor::pos(), QPoint(49, 24)); + + QFETCH(QPoint, targetPos); + quint32 timestamp = 1; + kwinApp()->platform()->pointerMotion(targetPos, timestamp++); + kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); + kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); + QCOMPARE(Cursor::pos(), targetPos); + QVERIFY(!workspace()->getMovingClient()); + + QCOMPARE(quickTileChangedSpy.count(), 1); + QTEST(c->quickTileMode(), "expectedMode"); + QTRY_COMPARE(sizeChangeSpy.count(), 1); +} + + +void QuickTilingTest::testQuickTilingPointerMoveXdgShell_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::testQuickTilingPointerMoveXdgShell() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createXdgShellV6Surface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + // let's render + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(c); + QCOMPARE(workspace()->activeClient(), c); + QCOMPARE(c->geometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(c->maximizeMode(), MaximizeRestore); + QVERIFY(configureRequestedSpy.wait()); + QTRY_COMPARE(configureRequestedSpy.count(), 2); + + QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + + workspace()->performWindowOperation(c, Options::UnrestrictedMoveOp); + QCOMPARE(c, workspace()->getMovingClient()); + QCOMPARE(Cursor::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(Cursor::pos(), targetPos); + QVERIFY(!workspace()->getMovingClient()); + + QCOMPARE(quickTileChangedSpy.count(), 1); + QTEST(c->quickTileMode(), "expectedMode"); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 4); + QCOMPARE(false, configureRequestedSpy.last().first().toSize().isEmpty()); +} + +void QuickTilingTest::testQuickTilingTouchMoveXdgShell_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::testQuickTilingTouchMoveXdgShell() +{ + // 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::createXdgShellV6Surface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + // let's render + 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->geometry(), QRect(-decoration->borderLeft(), 0, + 1000 + decoration->borderLeft() + decoration->borderRight(), + 50 + decoration->borderTop() + decoration->borderBottom())); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QCOMPARE(c->maximizeMode(), MaximizeRestore); + 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->geometry().center().x(), c->geometry().y() + decoration->borderTop() / 2), timestamp++); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(c, workspace()->getMovingClient()); + QCOMPARE(configureRequestedSpy.count(), 3); + + QFETCH(QPoint, targetPos); + kwinApp()->platform()->touchMotion(0, targetPos, timestamp++); + kwinApp()->platform()->touchUp(0, timestamp++); + QVERIFY(!workspace()->getMovingClient()); + + QCOMPARE(quickTileChangedSpy.count(), 1); + QTEST(c->quickTileMode(), "expectedMode"); + QVERIFY(configureRequestedSpy.wait()); + QTRY_COMPARE(configureRequestedSpy.count(), 5); + 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()); + Client *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->geometry(); + QFETCH(QuickTileMode, mode); + client->setQuickTileMode(mode, true); + QCOMPARE(client->quickTileMode(), mode); + QTEST(client->geometry(), "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, &Client::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()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + + const QRect origGeo = client->geometry(); + QCOMPARE(client->maximizeMode(), MaximizeRestore); + // vertically maximize the window + client->maximize(client->maximizeMode() ^ MaximizeVertical); + QCOMPARE(client->geometry().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->geometry(), "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, &Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +void QuickTilingTest::testShortcut_data() +{ + QTest::addColumn("shortcut"); + QTest::addColumn("expectedMode"); + QTest::addColumn("expectedGeometry"); + +#define FLAG(name) QuickTileMode(QuickTileFlag::name) + QTest::newRow("top") << QStringLiteral("Window Quick Tile Top") << FLAG(Top) << QRect(0, 0, 1280, 512); + QTest::newRow("left") << QStringLiteral("Window Quick Tile Left") << FLAG(Left) << QRect(0, 0, 640, 1024); + QTest::newRow("bottom") << QStringLiteral("Window Quick Tile Bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512); + QTest::newRow("right") << QStringLiteral("Window Quick Tile Right") << FLAG(Right) << QRect(640, 0, 640, 1024); + + QTest::newRow("top right") << QStringLiteral("Window Quick Tile Top Right") << (FLAG(Top) | FLAG(Right)) << QRect(640, 0, 640, 512); + QTest::newRow("top left") << QStringLiteral("Window Quick Tile Top Left") << (FLAG(Top) | FLAG(Left)) << QRect(0, 0, 640, 512); + QTest::newRow("bottom right") << QStringLiteral("Window Quick Tile Bottom Right") << (FLAG(Bottom) | FLAG(Right)) << QRect(640, 512, 640, 512); + QTest::newRow("bottom left") << QStringLiteral("Window Quick Tile Bottom Left") << (FLAG(Bottom) | FLAG(Left)) << QRect(0, 512, 640, 512); + +#undef FLAG +} + +void QuickTilingTest::testShortcut() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &ShellSurface::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->geometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); + QVERIFY(geometryChangedSpy.isValid()); + + QFETCH(QString, shortcut); + QFETCH(QuickTileMode, expectedMode); + QFETCH(QRect, expectedGeometry); + + // 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); + + QVERIFY(quickTileChangedSpy.wait()); + QCOMPARE(quickTileChangedSpy.count(), 1); + // at this point the geometry did not yet change + QCOMPARE(c->geometry(), QRect(0, 0, 100, 50)); + // but quick tile mode already changed + QCOMPARE(c->quickTileMode(), expectedMode); + + // but we got requested a new geometry + QTRY_COMPARE(sizeChangeSpy.count(), 1); + QCOMPARE(sizeChangeSpy.first().first().toSize(), expectedGeometry.size()); + + // attach a new image + Test::render(surface.data(), expectedGeometry.size(), Qt::red); + m_connection->flush(); + + QVERIFY(geometryChangedSpy.wait()); + QEXPECT_FAIL("maximize", "Geometry changed called twice for maximize", Continue); + QCOMPARE(geometryChangedSpy.count(), 1); + QCOMPARE(c->geometry(), 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("left") << QStringLiteral("Left") << FLAG(Left) << QRect(0, 0, 640, 1024); + QTest::newRow("bottom") << QStringLiteral("Bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512); + QTest::newRow("right") << QStringLiteral("Right") << FLAG(Right) << QRect(640, 0, 640, 1024); + + 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); + +#undef FLAG +} + +void QuickTilingTest::testScript() +{ + using namespace KWayland::Client; + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + QSignalSpy sizeChangeSpy(shellSurface.data(), &ShellSurface::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->geometry(), QRect(0, 0, 100, 50)); + QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); + QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); + QVERIFY(quickTileChangedSpy.isValid()); + QSignalSpy geometryChangedSpy(c, &AbstractClient::geometryChanged); + QVERIFY(geometryChangedSpy.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->geometry(), QRect(0, 0, 100, 50)); + // but quick tile mode already changed + QCOMPARE(c->quickTileMode(), expectedMode); + + // but we got requested a new geometry + QTRY_COMPARE(sizeChangeSpy.count(), 1); + QCOMPARE(sizeChangeSpy.first().first().toSize(), expectedGeometry.size()); + + // attach a new image + Test::render(surface.data(), expectedGeometry.size(), Qt::red); + m_connection->flush(); + + QVERIFY(geometryChangedSpy.wait()); + QEXPECT_FAIL("maximize", "Geometry changed called twice for maximize", Continue); + QCOMPARE(geometryChangedSpy.count(), 1); + QCOMPARE(c->geometry(), 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..1904322 --- /dev/null +++ b/autotests/integration/scene_opengl_es_test.cpp @@ -0,0 +1,30 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..79dd96f --- /dev/null +++ b/autotests/integration/scene_opengl_shadow_test.cpp @@ -0,0 +1,864 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#include + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "kwin_wayland_test.h" + +#include "composite.h" +#include "effect_builtins.h" +#include "effectloader.h" +#include "effects.h" +#include "platform.h" +#include "shadow.h" +#include "shell_client.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 + + if (!QFile::exists(QStringLiteral("/dev/dri/card0"))) { + QSKIP("Needs a dri device"); + } + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.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 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::createShellSurface(surface.data())); + QScopedPointer ssd(Test::waylandServerSideDecoration()->create(surface.data())); + + auto *client = Test::renderAndWaitForShown(surface.data(), windowSize, Qt::blue); + + QSignalSpy sizeChangedSpy(shellSurface.data(), &ShellSurface::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::createShellSurface(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(), &KWayland::Server::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::createShellSurface(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(), &KWayland::Server::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..faaed01 --- /dev/null +++ b/autotests/integration/scene_opengl_test.cpp @@ -0,0 +1,30 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..a28e7ea --- /dev/null +++ b/autotests/integration/scene_qpainter_shadow_test.cpp @@ -0,0 +1,782 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "kwin_wayland_test.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 "shell_client.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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.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::createShellSurface(surface.data())); + QScopedPointer ssd(Test::waylandServerSideDecoration()->create(surface.data())); + + auto *client = Test::renderAndWaitForShown(surface.data(), windowSize, Qt::blue); + + QSignalSpy sizeChangedSpy(shellSurface.data(), &ShellSurface::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::createShellSurface(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(), &KWayland::Server::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..2b696a8 --- /dev/null +++ b/autotests/integration/scene_qpainter_test.cpp @@ -0,0 +1,395 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "composite.h" +#include "effectloader.h" +#include "client.h" +#include "cursor.h" +#include "effects.h" +#include "platform.h" +#include "shell_client.h" +#include "wayland_server.h" +#include "effect_builtins.h" +#include "workspace.h" + +#include + +#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_data(); + void testWindow(); + void testWindowScaled(); + void testCompositorRestart_data(); + void testCompositorRestart(); + void testX11Window(); +}; + +void SceneQPainterTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void SceneQPainterTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.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); + 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); + const QImage cursorImage = kwinApp()->platform()->softwareCursor(); + QVERIFY(!cursorImage.isNull()); + p.drawImage(KWin::Cursor::pos() - kwinApp()->platform()->softwareCursorHotspot(), 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::Cursor::setPos(0, 0); + QVERIFY(frameRenderedSpy.wait()); + KWin::Cursor::setPos(10, 0); + QVERIFY(frameRenderedSpy.wait()); + KWin::Cursor::setPos(10, 12); + QVERIFY(frameRenderedSpy.wait()); + KWin::Cursor::setPos(12, 14); + QVERIFY(frameRenderedSpy.wait()); + KWin::Cursor::setPos(50, 60); + QVERIFY(frameRenderedSpy.wait()); + KWin::Cursor::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); + const QImage cursorImage = kwinApp()->platform()->softwareCursor(); + QVERIFY(!cursorImage.isNull()); + p.drawImage(QPoint(45, 45) - kwinApp()->platform()->softwareCursorHotspot(), cursorImage); + QCOMPARE(referenceImage, *scene->qpainterRenderBuffer()); +} + +void SceneQPainterTest::testWindow_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void SceneQPainterTest::testWindow() +{ + KWin::Cursor::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()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer ss(Test::createShellSurface(type, 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::Cursor::pos().x() - 5, KWin::Cursor::pos().y() - 5, 10, 10, Qt::red); + QCOMPARE(referenceImage, *scene->qpainterRenderBuffer()); + // let's move the cursor again + KWin::Cursor::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::Cursor::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::createShellSurface(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_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void SceneQPainterTest::testCompositorRestart() +{ + // this test verifies that the compositor/SceneQPainter survive a restart of the compositor and still render correctly + KWin::Cursor::setPos(400, 400); + + // first create a window + using namespace KWayland::Client; + QVERIFY(Test::setupWaylandConnection()); + QScopedPointer s(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer ss(Test::createShellSurface(type, 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()->slotReinitialize(); + 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); + const QImage cursorImage = kwinApp()->platform()->softwareCursor(); + QVERIFY(!cursorImage.isNull()); + painter.drawImage(QPoint(400, 400) - kwinApp()->platform()->softwareCursorHotspot(), 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 = defaultScreen()->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()); + Client *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->geometry().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, &Client::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..0a69112 --- /dev/null +++ b/autotests/integration/screen_changes_test.cpp @@ -0,0 +1,195 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); + setenv("QT_QPA_PLATFORM", "wayland", true); + waylandServer()->initWorkspace(); +} + +void ScreenChangesTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + + screens()->setCurrent(0); + KWin::Cursor::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()); + + // 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..06bcbac --- /dev/null +++ b/autotests/integration/screenedge_client_show_test.cpp @@ -0,0 +1,296 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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(workspaceCreatedSpy.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); + Cursor::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()); + Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->geometry(), 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, &Client::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); + Cursor::setPos(triggerPos); + QVERIFY(!client->isHiddenInternal()); + QCOMPARE(effectsWindowShownSpy.count(), 1); + + // go into event loop to trigger xcb_flush + QTest::qWait(1); + + //hide window again + Cursor::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->setGeometry(resizedWindowGeometry); + //triggerPos shouldn't be valid anymore + Cursor::setPos(triggerPos); + QVERIFY(client->isHiddenInternal()); + + // destroy window again + QSignalSpy windowClosedSpy(client, &Client::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()); + Client *client = windowCreatedSpy.last().first().value(); + QVERIFY(client); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->geometry(), 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, &Client::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, &Client::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..854f7ae --- /dev/null +++ b/autotests/integration/scripting/CMakeLists.txt @@ -0,0 +1 @@ +integrationTest(NAME testScriptingScreenEdge SRCS screenedge_test.cpp) diff --git a/autotests/integration/scripting/screenedge_test.cpp b/autotests/integration/scripting/screenedge_test.cpp new file mode 100644 index 0000000..1d85474 --- /dev/null +++ b/autotests/integration/scripting/screenedge_test.cpp @@ -0,0 +1,298 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.wait()); + QVERIFY(Scripting::self()); + + ScreenEdges::self()->setTimeThreshold(0); + ScreenEdges::self()->setReActivationThreshold(0); +} + +void ScreenEdgeTest::init() +{ + KWin::Cursor::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::Cursor::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::Cursor::setPos(triggerPos); + QCOMPARE(showDesktopSpy.count(), 1); + + //reset + KWin::Cursor::setPos(500,500); + workspace()->slotToggleShowDesktop(); + showDesktopSpy.clear(); + + //trigger again, to show that retriggering works + KWin::Cursor::setPos(triggerPos); + QCOMPARE(showDesktopSpy.count(), 1); + + //reset + KWin::Cursor::setPos(500,500); + workspace()->slotToggleShowDesktop(); + showDesktopSpy.clear(); + + //make the script unregister the edge + configGroup.writeEntry("mode", "unregister"); + triggerConfigReload(); + KWin::Cursor::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..787f654 --- /dev/null +++ b/autotests/integration/shade_test.cpp @@ -0,0 +1,143 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "client.h" +#include "cursor.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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); + Cursor::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()); + Client *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->geometry(); + QVERIFY(geoBeforeShade.isValid()); + QVERIFY(!geoBeforeShade.isEmpty()); + workspace()->slotWindowShade(); + QVERIFY(client->isShade()); + QVERIFY(client->geometry() != geoBeforeShade); + // and unshade again + workspace()->slotWindowShade(); + QVERIFY(!client->isShade()); + QCOMPARE(client->geometry(), 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, &Client::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + QVERIFY(windowClosedSpy.wait()); +} + +} + +WAYLANDTEST_MAIN(KWin::ShadeTest) +#include "shade_test.moc" diff --git a/autotests/integration/shell_client_rules_test.cpp b/autotests/integration/shell_client_rules_test.cpp new file mode 100644 index 0000000..2321f79 --- /dev/null +++ b/autotests/integration/shell_client_rules_test.cpp @@ -0,0 +1,454 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "rules.h" +#include "screens.h" +#include "shell_client.h" +#include "virtualdesktops.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_shell_client_rules-0"); + + +class TestShellClientRules : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testApplyInitialDesktop_data(); + void testApplyInitialDesktop(); + void testApplyInitialMinimize_data(); + void testApplyInitialMinimize(); + void testApplyInitialSkipTaskbar_data(); + void testApplyInitialSkipTaskbar(); + void testApplyInitialSkipPager_data(); + void testApplyInitialSkipPager(); + void testApplyInitialSkipSwitcher_data(); + void testApplyInitialSkipSwitcher(); + void testApplyInitialKeepAbove_data(); + void testApplyInitialKeepAbove(); + void testApplyInitialKeepBelow_data(); + void testApplyInitialKeepBelow(); + void testApplyInitialShortcut_data(); + void testApplyInitialShortcut(); + void testApplyInitialDesktopfile_data(); + void testApplyInitialDesktopfile(); + void testOpacityActive_data(); + void testOpacityActive(); + void testMatchAfterNameChange(); +}; + +void TestShellClientRules::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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 TestShellClientRules::init() +{ + VirtualDesktopManager::self()->setCurrent(VirtualDesktopManager::self()->desktops().first()); + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); + + screens()->setCurrent(0); +} + +void TestShellClientRules::cleanup() +{ + Test::destroyWaylandConnection(); +} + +#define TEST_DATA( name ) \ +void TestShellClientRules::name##_data() \ +{ \ + QTest::addColumn("type"); \ + QTest::addColumn("ruleNumber"); \ + QTest::newRow("wlShell|Force") << Test::ShellSurfaceType::WlShell << 2; \ + QTest::newRow("xdgShellV5|Force") << Test::ShellSurfaceType::XdgShellV5 << 2; \ + QTest::newRow("xdgShellV6|Force") << Test::ShellSurfaceType::XdgShellV6 << 2; \ + QTest::newRow("wlShell|Apply") << Test::ShellSurfaceType::WlShell << 3; \ + QTest::newRow("xdgShellV5|Apply") << Test::ShellSurfaceType::XdgShellV5 << 3; \ + QTest::newRow("xdgShellV6|Apply") << Test::ShellSurfaceType::XdgShellV6 << 3; \ + QTest::newRow("wlShell|ApplyNow") << Test::ShellSurfaceType::WlShell << 5; \ + QTest::newRow("xdgShellV5|ApplyNow") << Test::ShellSurfaceType::XdgShellV5 << 5; \ + QTest::newRow("xdgShellV6|ApplyNow") << Test::ShellSurfaceType::XdgShellV6 << 5; \ + QTest::newRow("wlShell|ForceTemporarily") << Test::ShellSurfaceType::WlShell << 6; \ + QTest::newRow("xdgShellV5|ForceTemporarily") << Test::ShellSurfaceType::XdgShellV5 << 6; \ + QTest::newRow("xdgShellV6|ForceTemporarily") << Test::ShellSurfaceType::XdgShellV6 << 6; \ +} + +#define TEST_FORCE_DATA( name ) \ +void TestShellClientRules::name##_data() \ +{ \ + QTest::addColumn("type"); \ + QTest::addColumn("ruleNumber"); \ + QTest::newRow("wlShell|Force") << Test::ShellSurfaceType::WlShell << 2; \ + QTest::newRow("xdgShellV5|Force") << Test::ShellSurfaceType::XdgShellV5 << 2; \ + QTest::newRow("xdgShellV6|Force") << Test::ShellSurfaceType::XdgShellV6 << 2; \ + QTest::newRow("wlShell|ForceTemporarily") << Test::ShellSurfaceType::WlShell << 6; \ + QTest::newRow("xdgShellV5|ForceTemporarily") << Test::ShellSurfaceType::XdgShellV5 << 6; \ + QTest::newRow("xdgShellV6|ForceTemporarily") << Test::ShellSurfaceType::XdgShellV6 << 6; \ +} + + +TEST_DATA(testApplyInitialDesktop) + +void TestShellClientRules::testApplyInitialDesktop() +{ + // ensure we have two desktops and are on first desktop + VirtualDesktopManager::self()->setCount(2); + VirtualDesktopManager::self()->setCurrent(VirtualDesktopManager::self()->desktops().first()); + + // install the temporary rule + QFETCH(int, ruleNumber); + QString rule = QStringLiteral("desktop=2\ndesktoprule=%1").arg(ruleNumber); + QMetaObject::invokeMethod(RuleBook::self(), "temporaryRulesMessage", Q_ARG(QString, rule)); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->desktop(), 2); + QCOMPARE(c->isMinimized(), false); + QCOMPARE(c->isActive(), true); + QCOMPARE(c->skipTaskbar(), false); + QCOMPARE(c->skipPager(), false); + QCOMPARE(c->skipSwitcher(), false); + QCOMPARE(c->keepAbove(), false); + QCOMPARE(c->keepBelow(), false); + QCOMPARE(c->shortcut(), QKeySequence()); +} + +TEST_DATA(testApplyInitialMinimize) + +void TestShellClientRules::testApplyInitialMinimize() +{ + // install the temporary rule + QFETCH(int, ruleNumber); + QString rule = QStringLiteral("minimize=1\nminimizerule=%1").arg(ruleNumber); + QMetaObject::invokeMethod(RuleBook::self(), "temporaryRulesMessage", Q_ARG(QString, rule)); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->desktop(), 1); + QCOMPARE(c->isMinimized(), true); + QCOMPARE(c->isActive(), false); + c->setMinimized(false); + QCOMPARE(c->isMinimized(), false); + QCOMPARE(c->skipTaskbar(), false); + QCOMPARE(c->skipPager(), false); + QCOMPARE(c->skipSwitcher(), false); + QCOMPARE(c->keepAbove(), false); + QCOMPARE(c->keepBelow(), false); + QCOMPARE(c->shortcut(), QKeySequence()); +} + +TEST_DATA(testApplyInitialSkipTaskbar) + +void TestShellClientRules::testApplyInitialSkipTaskbar() +{ + // install the temporary rule + QFETCH(int, ruleNumber); + QString rule = QStringLiteral("skiptaskbar=true\nskiptaskbarrule=%1").arg(ruleNumber); + QMetaObject::invokeMethod(RuleBook::self(), "temporaryRulesMessage", Q_ARG(QString, rule)); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->desktop(), 1); + QCOMPARE(c->isMinimized(), false); + QCOMPARE(c->isActive(), true); + QCOMPARE(c->skipTaskbar(), true); + QCOMPARE(c->skipPager(), false); + QCOMPARE(c->skipSwitcher(), false); + QCOMPARE(c->keepAbove(), false); + QCOMPARE(c->keepBelow(), false); + QCOMPARE(c->shortcut(), QKeySequence()); +} + +TEST_DATA(testApplyInitialSkipPager) + +void TestShellClientRules::testApplyInitialSkipPager() +{ + // install the temporary rule + QFETCH(int, ruleNumber); + QString rule = QStringLiteral("skippager=true\nskippagerrule=%1").arg(ruleNumber); + QMetaObject::invokeMethod(RuleBook::self(), "temporaryRulesMessage", Q_ARG(QString, rule)); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->desktop(), 1); + QCOMPARE(c->isMinimized(), false); + QCOMPARE(c->isActive(), true); + QCOMPARE(c->skipTaskbar(), false); + QCOMPARE(c->skipPager(), true); + QCOMPARE(c->skipSwitcher(), false); + QCOMPARE(c->keepAbove(), false); + QCOMPARE(c->keepBelow(), false); + QCOMPARE(c->shortcut(), QKeySequence()); +} + +TEST_DATA(testApplyInitialSkipSwitcher) + +void TestShellClientRules::testApplyInitialSkipSwitcher() +{ + // install the temporary rule + QFETCH(int, ruleNumber); + QString rule = QStringLiteral("skipswitcher=true\nskipswitcherrule=%1").arg(ruleNumber); + QMetaObject::invokeMethod(RuleBook::self(), "temporaryRulesMessage", Q_ARG(QString, rule)); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->desktop(), 1); + QCOMPARE(c->isMinimized(), false); + QCOMPARE(c->isActive(), true); + QCOMPARE(c->skipTaskbar(), false); + QCOMPARE(c->skipPager(), false); + QCOMPARE(c->skipSwitcher(), true); + QCOMPARE(c->keepAbove(), false); + QCOMPARE(c->keepBelow(), false); + QCOMPARE(c->shortcut(), QKeySequence()); +} + +TEST_DATA(testApplyInitialKeepAbove) + +void TestShellClientRules::testApplyInitialKeepAbove() +{ + // install the temporary rule + QFETCH(int, ruleNumber); + QString rule = QStringLiteral("above=true\naboverule=%1").arg(ruleNumber); + QMetaObject::invokeMethod(RuleBook::self(), "temporaryRulesMessage", Q_ARG(QString, rule)); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->desktop(), 1); + QCOMPARE(c->isMinimized(), false); + QCOMPARE(c->isActive(), true); + QCOMPARE(c->skipTaskbar(), false); + QCOMPARE(c->skipPager(), false); + QCOMPARE(c->skipSwitcher(), false); + QCOMPARE(c->keepAbove(), true); + QCOMPARE(c->keepBelow(), false); + QCOMPARE(c->shortcut(), QKeySequence()); +} + +TEST_DATA(testApplyInitialKeepBelow) + +void TestShellClientRules::testApplyInitialKeepBelow() +{ + // install the temporary rule + QFETCH(int, ruleNumber); + QString rule = QStringLiteral("below=true\nbelowrule=%1").arg(ruleNumber); + QMetaObject::invokeMethod(RuleBook::self(), "temporaryRulesMessage", Q_ARG(QString, rule)); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->desktop(), 1); + QCOMPARE(c->isMinimized(), false); + QCOMPARE(c->isActive(), true); + QCOMPARE(c->skipTaskbar(), false); + QCOMPARE(c->skipPager(), false); + QCOMPARE(c->skipSwitcher(), false); + QCOMPARE(c->keepAbove(), false); + QCOMPARE(c->keepBelow(), true); + QCOMPARE(c->shortcut(), QKeySequence()); +} + +TEST_DATA(testApplyInitialShortcut) + +void TestShellClientRules::testApplyInitialShortcut() +{ + // install the temporary rule + QFETCH(int, ruleNumber); + const QKeySequence sequence{Qt::ControlModifier + Qt::ShiftModifier + Qt::MetaModifier + Qt::AltModifier + Qt::Key_Space}; + QString rule = QStringLiteral("shortcut=%1\nshortcutrule=%2").arg(sequence.toString()).arg(ruleNumber); + QMetaObject::invokeMethod(RuleBook::self(), "temporaryRulesMessage", Q_ARG(QString, rule)); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->desktop(), 1); + QCOMPARE(c->isMinimized(), false); + QCOMPARE(c->isActive(), true); + QCOMPARE(c->skipTaskbar(), false); + QCOMPARE(c->skipPager(), false); + QCOMPARE(c->skipSwitcher(), false); + QCOMPARE(c->keepAbove(), false); + QCOMPARE(c->keepBelow(), false); + QCOMPARE(c->shortcut(), sequence); +} + +TEST_DATA(testApplyInitialDesktopfile) + +void TestShellClientRules::testApplyInitialDesktopfile() +{ + // install the temporary rule + QFETCH(int, ruleNumber); + QString rule = QStringLiteral("desktopfile=org.kde.kwin\ndesktopfilerule=%1").arg(ruleNumber); + QMetaObject::invokeMethod(RuleBook::self(), "temporaryRulesMessage", Q_ARG(QString, rule)); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QCOMPARE(c->desktop(), 1); + QCOMPARE(c->isMinimized(), false); + QCOMPARE(c->isActive(), true); + QCOMPARE(c->skipTaskbar(), false); + QCOMPARE(c->skipPager(), false); + QCOMPARE(c->skipSwitcher(), false); + QCOMPARE(c->keepAbove(), false); + QCOMPARE(c->keepBelow(), false); + QCOMPARE(c->shortcut(), QKeySequence()); + QCOMPARE(c->desktopFileName(), QByteArrayLiteral("org.kde.kwin")); +} + +TEST_FORCE_DATA(testOpacityActive) + +void TestShellClientRules::testOpacityActive() +{ + KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + config->group("General").writeEntry("count", 1); + + auto group = config->group("1"); + group.writeEntry("opacityactive", 90); + group.writeEntry("opacityinactive", 80); + QFETCH(int, ruleNumber); + group.writeEntry("opacityactiverule", ruleNumber); + group.writeEntry("opacityinactiverule", ruleNumber); + group.sync(); + + RuleBook::self()->setConfig(config); + workspace()->slotReconfigure(); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QCOMPARE(c->opacity(), 0.9); + + // open a second window + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createShellSurface(type, surface2.data())); + + auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); + QVERIFY(c2); + QVERIFY(c2->isActive()); + QVERIFY(!c->isActive()); + QCOMPARE(c2->opacity(), 0.9); + QCOMPARE(c->opacity(), 0.8); + + workspace()->activateClient(c); + QVERIFY(!c2->isActive()); + QVERIFY(c->isActive()); + QCOMPARE(c->opacity(), 0.9); + QCOMPARE(c2->opacity(), 0.8); +} + +void TestShellClientRules::testMatchAfterNameChange() +{ + 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(); + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createXdgShellV6Surface(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(TestShellClientRules) +#include "shell_client_rules_test.moc" diff --git a/autotests/integration/shell_client_test.cpp b/autotests/integration/shell_client_test.cpp new file mode 100644 index 0000000..c69291a --- /dev/null +++ b/autotests/integration/shell_client_test.cpp @@ -0,0 +1,1020 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "cursor.h" +#include "effects.h" +#include "platform.h" +#include "shell_client.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + + +// system +#include +#include +#include + +using namespace KWin; +using namespace KWayland::Client; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_shell_client-0"); + +class TestShellClient : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testMapUnmapMap_data(); + void testMapUnmapMap(); + void testDesktopPresenceChanged(); + void testTransientPositionAfterRemap(); + void testWindowOutputs_data(); + void testWindowOutputs(); + void testMinimizeActiveWindow_data(); + void testMinimizeActiveWindow(); + void testFullscreen_data(); + void testFullscreen(); + void testUserCanSetFullscreen_data(); + void testUserCanSetFullscreen(); + void testUserSetFullscreenWlShell(); + void testUserSetFullscreenXdgShell_data(); + void testUserSetFullscreenXdgShell(); + void testMaximizedToFullscreen_data(); + void testMaximizedToFullscreen(); + void testWindowOpensLargerThanScreen_data(); + void testWindowOpensLargerThanScreen(); + void testHidden_data(); + void testHidden(); + void testDesktopFileName(); + void testCaptionSimplified(); + void testCaptionMultipleWindows(); + void testUnresponsiveWindow_data(); + void testUnresponsiveWindow(); + void testX11WindowId_data(); + void testX11WindowId(); + void testAppMenu(); + void testNoDecorationModeRequested_data(); + void testNoDecorationModeRequested(); +}; + +void TestShellClient::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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 TestShellClient::init() +{ + QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration | + Test::AdditionalWaylandInterface::AppMenu)); + + screens()->setCurrent(0); + KWin::Cursor::setPos(QPoint(1280, 512)); +} + +void TestShellClient::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TestShellClient::testMapUnmapMap_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void TestShellClient::testMapUnmapMap() +{ + // this test verifies that mapping a previously mapped window works correctly + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + QSignalSpy effectsWindowShownSpy(effects, &EffectsHandler::windowShown); + QVERIFY(effectsWindowShownSpy.isValid()); + QSignalSpy effectsWindowHiddenSpy(effects, &EffectsHandler::windowHidden); + QVERIFY(effectsWindowHiddenSpy.isValid()); + + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + // 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); + QVERIFY(client->isShown(true)); + QCOMPARE(client->isHiddenInternal(), false); + QCOMPARE(client->readyForPainting(), true); + QCOMPARE(client->depth(), 32); + QVERIFY(client->hasAlpha()); + QCOMPARE(client->icon().name(), QStringLiteral("wayland")); + QCOMPARE(workspace()->activeClient(), client); + QVERIFY(effectsWindowShownSpy.isEmpty()); + QVERIFY(client->isMaximizable()); + QVERIFY(client->isMovable()); + QVERIFY(client->isMovableAcrossScreens()); + QVERIFY(client->isResizable()); + QVERIFY(client->property("maximizable").toBool()); + QVERIFY(client->property("moveable").toBool()); + QVERIFY(client->property("moveableAcrossScreens").toBool()); + QVERIFY(client->property("resizeable").toBool()); + + // now unmap + QSignalSpy hiddenSpy(client, &ShellClient::windowHidden); + QVERIFY(hiddenSpy.isValid()); + QSignalSpy windowClosedSpy(client, &ShellClient::windowClosed); + QVERIFY(windowClosedSpy.isValid()); + surface->attachBuffer(Buffer::Ptr()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(hiddenSpy.wait()); + QCOMPARE(client->readyForPainting(), true); + QCOMPARE(client->isHiddenInternal(), true); + QVERIFY(windowClosedSpy.isEmpty()); + QVERIFY(!workspace()->activeClient()); + QCOMPARE(effectsWindowHiddenSpy.count(), 1); + QCOMPARE(effectsWindowHiddenSpy.first().first().value(), client->effectWindow()); + + QSignalSpy windowShownSpy(client, &ShellClient::windowShown); + QVERIFY(windowShownSpy.isValid()); + Test::render(surface.data(), QSize(100, 50), Qt::blue, QImage::Format_RGB32); + QCOMPARE(clientAddedSpy.count(), 1); + QVERIFY(windowShownSpy.wait()); + QCOMPARE(windowShownSpy.count(), 1); + QCOMPARE(clientAddedSpy.count(), 1); + QCOMPARE(client->readyForPainting(), true); + QCOMPARE(client->isHiddenInternal(), false); + QCOMPARE(client->depth(), 24); + QVERIFY(!client->hasAlpha()); + QCOMPARE(workspace()->activeClient(), client); + QCOMPARE(effectsWindowShownSpy.count(), 1); + QCOMPARE(effectsWindowShownSpy.first().first().value(), client->effectWindow()); + + // let's unmap again + surface->attachBuffer(Buffer::Ptr()); + surface->commit(Surface::CommitFlag::None); + QVERIFY(hiddenSpy.wait()); + QCOMPARE(hiddenSpy.count(), 2); + QCOMPARE(client->readyForPainting(), true); + QCOMPARE(client->isHiddenInternal(), true); + QVERIFY(windowClosedSpy.isEmpty()); + QCOMPARE(effectsWindowHiddenSpy.count(), 2); + QCOMPARE(effectsWindowHiddenSpy.last().first().value(), client->effectWindow()); + + shellSurface.reset(); + surface.reset(); + QVERIFY(windowClosedSpy.wait()); + QCOMPARE(windowClosedSpy.count(), 1); + QCOMPARE(effectsWindowHiddenSpy.count(), 2); +} + +void TestShellClient::testDesktopPresenceChanged() +{ + // this test verifies that the desktop presence changed signals are properly emitted + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(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, &ShellClient::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 TestShellClient::testTransientPositionAfterRemap() +{ + // this test simulates the situation that a transient window gets reused and the parent window + // moved between the two usages + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + + // create the Transient window + QScopedPointer transientSurface(Test::createSurface()); + QScopedPointer transientShellSurface(Test::createShellSurface(transientSurface.data())); + transientShellSurface->setTransient(surface.data(), QPoint(5, 10)); + auto transient = Test::renderAndWaitForShown(transientSurface.data(), QSize(50, 40), Qt::blue); + QVERIFY(transient); + QCOMPARE(transient->geometry(), QRect(c->geometry().topLeft() + QPoint(5, 10), QSize(50, 40))); + + // unmap the transient + QSignalSpy windowHiddenSpy(transient, &ShellClient::windowHidden); + QVERIFY(windowHiddenSpy.isValid()); + transientSurface->attachBuffer(Buffer::Ptr()); + transientSurface->commit(Surface::CommitFlag::None); + QVERIFY(windowHiddenSpy.wait()); + + // now move the parent surface + c->setGeometry(c->geometry().translated(5, 10)); + + // now map the transient again + QSignalSpy windowShownSpy(transient, &ShellClient::windowShown); + QVERIFY(windowShownSpy.isValid()); + Test::render(transientSurface.data(), QSize(50, 40), Qt::blue); + QVERIFY(windowShownSpy.wait()); + QCOMPARE(transient->geometry(), QRect(c->geometry().topLeft() + QPoint(5, 10), QSize(50, 40))); +} + +void TestShellClient::testWindowOutputs_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void TestShellClient::testWindowOutputs() +{ + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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->setGeometry(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->setGeometry(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->setGeometry(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 TestShellClient::testMinimizeActiveWindow_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void TestShellClient::testMinimizeActiveWindow() +{ + // this test verifies that when minimizing the active window it gets deactivated + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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 TestShellClient::testFullscreen_data() +{ + QTest::addColumn("type"); + QTest::addColumn("decoMode"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell << ServerSideDecoration::Mode::Client; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5 << ServerSideDecoration::Mode::Client; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6 << ServerSideDecoration::Mode::Client; + + + QTest::newRow("wlShell - deco") << Test::ShellSurfaceType::WlShell << ServerSideDecoration::Mode::Server; + QTest::newRow("xdgShellV5 - deco") << Test::ShellSurfaceType::XdgShellV5 << ServerSideDecoration::Mode::Server; + QTest::newRow("xdgShellV6 - deco") << Test::ShellSurfaceType::XdgShellV6 << ServerSideDecoration::Mode::Server; +} + +void TestShellClient::testFullscreen() +{ + // this test verifies that a window can be properly fullscreened + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + // 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 c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QCOMPARE(c->layer(), NormalLayer); + QVERIFY(!c->isFullScreen()); + QCOMPARE(c->clientSize(), QSize(100, 50)); + QCOMPARE(c->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QCOMPARE(c->sizeForClientSize(c->clientSize()), c->geometry().size()); + QSignalSpy fullscreenChangedSpy(c, &ShellClient::fullScreenChanged); + QVERIFY(fullscreenChangedSpy.isValid()); + QSignalSpy geometryChangedSpy(c, &ShellClient::geometryChanged); + QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy sizeChangeRequestedSpy(shellSurface.data(), SIGNAL(sizeChanged(QSize))); + QVERIFY(sizeChangeRequestedSpy.isValid()); + + // fullscreen the window + switch (type) { + case Test::ShellSurfaceType::WlShell: + qobject_cast(shellSurface.data())->setFullscreen(); + break; + case Test::ShellSurfaceType::XdgShellV5: + case Test::ShellSurfaceType::XdgShellV6: + qobject_cast(shellSurface.data())->setFullscreen(true); + break; + default: + Q_UNREACHABLE(); + break; + } + QVERIFY(fullscreenChangedSpy.wait()); + QVERIFY(sizeChangeRequestedSpy.wait()); + QCOMPARE(sizeChangeRequestedSpy.count(), 1); + QCOMPARE(sizeChangeRequestedSpy.first().first().toSize(), QSize(screens()->size(0))); + // TODO: should switch to fullscreen once it's updated + QVERIFY(c->isFullScreen()); + QCOMPARE(c->clientSize(), QSize(100, 50)); + QVERIFY(geometryChangedSpy.isEmpty()); + + // render at the new size + Test::render(surface.data(), sizeChangeRequestedSpy.first().first().toSize(), Qt::red); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(geometryChangedSpy.count(), 1); + QVERIFY(c->isFullScreen()); + QVERIFY(!c->isDecorated()); + QCOMPARE(c->geometry(), QRect(QPoint(0, 0), sizeChangeRequestedSpy.first().first().toSize())); + QCOMPARE(c->layer(), ActiveLayer); + + // swap back to normal + switch (type) { + case Test::ShellSurfaceType::WlShell: + qobject_cast(shellSurface.data())->setToplevel(); + break; + case Test::ShellSurfaceType::XdgShellV5: + case Test::ShellSurfaceType::XdgShellV6: + qobject_cast(shellSurface.data())->setFullscreen(false); + break; + default: + Q_UNREACHABLE(); + break; + } + QVERIFY(fullscreenChangedSpy.wait()); + QVERIFY(sizeChangeRequestedSpy.wait()); + QCOMPARE(sizeChangeRequestedSpy.count(), 2); + QCOMPARE(sizeChangeRequestedSpy.last().first().toSize(), QSize(100, 50)); + // TODO: should switch to fullscreen once it's updated + QVERIFY(!c->isFullScreen()); + QCOMPARE(c->layer(), NormalLayer); + QCOMPARE(c->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); +} + +void TestShellClient::testUserCanSetFullscreen_data() +{ + QTest::addColumn("type"); + QTest::addColumn("expected"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell << false; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5 << true; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6 << true; +} + +void TestShellClient::testUserCanSetFullscreen() +{ + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QVERIFY(!c->isFullScreen()); + QTEST(c->userCanSetFullScreen(), "expected"); +} + +void TestShellClient::testUserSetFullscreenWlShell() +{ + // wlshell cannot sync fullscreen to the client + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QVERIFY(!c->isFullScreen()); + QSignalSpy fullscreenChangedSpy(c, &AbstractClient::fullScreenChanged); + QVERIFY(fullscreenChangedSpy.isValid()); + c->setFullScreen(true); + QCOMPARE(fullscreenChangedSpy.count(), 0); + QVERIFY(!c->isFullScreen()); +} + +void TestShellClient::testUserSetFullscreenXdgShell_data() +{ + QTest::addColumn("type"); + + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void TestShellClient::testUserSetFullscreenXdgShell() +{ + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(dynamic_cast(Test::createShellSurface(type, surface.data()))); + QVERIFY(!shellSurface.isNull()); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QVERIFY(!c->isFullScreen()); + + // two, one for initial sync, second as it becomes active + QTRY_COMPARE(configureRequestedSpy.count(), 2); + + 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 TestShellClient::testMaximizedToFullscreen_data() +{ + QTest::addColumn("type"); + QTest::addColumn("decoMode"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell << ServerSideDecoration::Mode::Client; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5 << ServerSideDecoration::Mode::Client; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6 << ServerSideDecoration::Mode::Client; + + QTest::newRow("wlShell - deco") << Test::ShellSurfaceType::WlShell << ServerSideDecoration::Mode::Server; + QTest::newRow("xdgShellV5 - deco") << Test::ShellSurfaceType::XdgShellV5 << ServerSideDecoration::Mode::Server; + QTest::newRow("xdgShellV6 - deco") << Test::ShellSurfaceType::XdgShellV6 << ServerSideDecoration::Mode::Server; +} + +void TestShellClient::testMaximizedToFullscreen() +{ + // this test verifies that a window can be properly fullscreened after maximizing + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + + // 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 c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->isActive()); + QVERIFY(!c->isFullScreen()); + QCOMPARE(c->clientSize(), QSize(100, 50)); + QCOMPARE(c->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QSignalSpy fullscreenChangedSpy(c, &ShellClient::fullScreenChanged); + QVERIFY(fullscreenChangedSpy.isValid()); + QSignalSpy geometryChangedSpy(c, &ShellClient::geometryChanged); + QVERIFY(geometryChangedSpy.isValid()); + QSignalSpy sizeChangeRequestedSpy(shellSurface.data(), SIGNAL(sizeChanged(QSize))); + QVERIFY(sizeChangeRequestedSpy.isValid()); + + // change to maximize + switch (type) { + case Test::ShellSurfaceType::WlShell: + qobject_cast(shellSurface.data())->setMaximized(); + break; + case Test::ShellSurfaceType::XdgShellV5: + case Test::ShellSurfaceType::XdgShellV6: + qobject_cast(shellSurface.data())->setMaximized(true); + break; + default: + Q_UNREACHABLE(); + break; + } + QVERIFY(sizeChangeRequestedSpy.wait()); + QCOMPARE(sizeChangeRequestedSpy.count(), 1); + QCOMPARE(c->maximizeMode(), MaximizeFull); + QCOMPARE(geometryChangedSpy.isEmpty(), false); + geometryChangedSpy.clear(); + + // fullscreen the window + switch (type) { + case Test::ShellSurfaceType::WlShell: + qobject_cast(shellSurface.data())->setFullscreen(); + break; + case Test::ShellSurfaceType::XdgShellV5: + case Test::ShellSurfaceType::XdgShellV6: + qobject_cast(shellSurface.data())->setFullscreen(true); + break; + default: + Q_UNREACHABLE(); + break; + } + QVERIFY(fullscreenChangedSpy.wait()); + if (decoMode == ServerSideDecoration::Mode::Server) { + QVERIFY(sizeChangeRequestedSpy.wait()); + QCOMPARE(sizeChangeRequestedSpy.count(), 2); + } + QCOMPARE(sizeChangeRequestedSpy.last().first().toSize(), QSize(screens()->size(0))); + // TODO: should switch to fullscreen once it's updated + QVERIFY(c->isFullScreen()); + QCOMPARE(c->clientSize(), QSize(100, 50)); + QVERIFY(geometryChangedSpy.isEmpty()); + + // render at the new size + Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(geometryChangedSpy.count(), 1); + QVERIFY(c->isFullScreen()); + QVERIFY(!c->isDecorated()); + QCOMPARE(c->geometry(), QRect(QPoint(0, 0), sizeChangeRequestedSpy.last().first().toSize())); + sizeChangeRequestedSpy.clear(); + + // swap back to normal + switch (type) { + case Test::ShellSurfaceType::WlShell: + qobject_cast(shellSurface.data())->setToplevel(); + break; + case Test::ShellSurfaceType::XdgShellV5: + case Test::ShellSurfaceType::XdgShellV6: + qobject_cast(shellSurface.data())->setFullscreen(false); + break; + default: + Q_UNREACHABLE(); + break; + } + QVERIFY(fullscreenChangedSpy.wait()); + QVERIFY(sizeChangeRequestedSpy.wait()); + QCOMPARE(sizeChangeRequestedSpy.count(), 1); + QEXPECT_FAIL("wlShell - deco", "With decoration incorrect geometry requested", Continue); + QEXPECT_FAIL("xdgShellV5 - deco", "With decoration incorrect geometry requested", Continue); + QEXPECT_FAIL("xdgShellV6 - deco", "With decoration incorrect geometry requested", Continue); + QCOMPARE(sizeChangeRequestedSpy.last().first().toSize(), QSize(100, 50)); + // TODO: should switch to fullscreen once it's updated + QVERIFY(!c->isFullScreen()); + QCOMPARE(c->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); +} + +void TestShellClient::testWindowOpensLargerThanScreen_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void TestShellClient::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()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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()); + QCOMPARE(c->clientSize(), screens()->size(0)); + QVERIFY(c->isDecorated()); + QEXPECT_FAIL("", "BUG 366632", Continue); + QVERIFY(sizeChangeRequestedSpy.wait()); +} + +void TestShellClient::testHidden_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void TestShellClient::testHidden() +{ + // this test verifies that when hiding window it doesn't get shown + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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 TestShellClient::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(qobject_cast(Test::createShellSurface(Test::ShellSurfaceType::XdgShellV5, 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("testShellClient")); + // 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, &ShellClient::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("testShellClient")); + // 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 TestShellClient::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(qobject_cast(Test::createShellSurface(Test::ShellSurfaceType::XdgShellV5, 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 TestShellClient::testCaptionMultipleWindows() +{ + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(qobject_cast(Test::createShellSurface(Test::ShellSurfaceType::XdgShellV5, 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(qobject_cast(Test::createShellSurface(Test::ShellSurfaceType::XdgShellV5, 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(qobject_cast(Test::createShellSurface(Test::ShellSurfaceType::XdgShellV5, 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(qobject_cast(Test::createShellSurface(Test::ShellSurfaceType::XdgShellV5, 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, &ShellClient::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 TestShellClient::testUnresponsiveWindow_data() +{ + QTest::addColumn("shellInterface");//see env selection in qwaylandintegration.cpp + QTest::addColumn("socketMode"); + + //wl-shell ping is not implemented + //QTest::newRow("wl-shell display") << "wl-shell" << false; + //QTest::newRow("wl-shell socket") << "wl-shell" << true; + QTest::newRow("xdgv5 display") << "xdg-shell-v5" << false; + QTest::newRow("xdgv5 socket") << "xdg-shell-v5" << true; + QTest::newRow("xdgv6 display") << "xdg-shell-v6" << false; + QTest::newRow("xdgv6 socket") << "xdg-shell-v6" << true; +} + +void TestShellClient::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 shellClientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(shellClientAddedSpy.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); + process->start(); + QVERIFY(process->waitForStarted()); + + AbstractClient *killClient = nullptr; + QVERIFY(shellClientAddedSpy.wait()); + killClient = shellClientAddedSpy.first().first().value(); + 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 TestShellClient::testX11WindowId_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void TestShellClient::testX11WindowId() +{ + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(c); + QVERIFY(c->windowId() != 0); + QCOMPARE(c->window(), 0u); +} + +void TestShellClient::testAppMenu() +{ + //register a faux appmenu client + QVERIFY (QDBusConnection::sessionBus().registerService("org.kde.kappmenu")); + + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(Test::ShellSurfaceType::XdgShellV6, 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, &ShellClient::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 TestShellClient::testNoDecorationModeRequested_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +void TestShellClient::testNoDecorationModeRequested() +{ + // this test verifies that the decoration follows the default mode if no mode is explicitly requested + QScopedPointer surface(Test::createSurface()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, 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); +} + +WAYLANDTEST_MAIN(TestShellClient) +#include "shell_client_test.moc" diff --git a/autotests/integration/showing_desktop_test.cpp b/autotests/integration/showing_desktop_test.cpp new file mode 100644 index 0000000..6053479 --- /dev/null +++ b/autotests/integration/showing_desktop_test.cpp @@ -0,0 +1,128 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "shell_client.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_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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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::createShellSurface(surface1.data())); + auto client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createShellSurface(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::createShellSurface(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::createShellSurface(surface1.data())); + auto client1 = Test::renderAndWaitForShown(surface1.data(), QSize(100, 50), Qt::blue); + QScopedPointer surface2(Test::createSurface()); + QScopedPointer shellSurface2(Test::createShellSurface(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/start_test.cpp b/autotests/integration/start_test.cpp new file mode 100644 index 0000000..894d077 --- /dev/null +++ b/autotests/integration/start_test.cpp @@ -0,0 +1,163 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.h" + +#include +#include +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_start_test-0"); + +class StartTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanup(); + void testScreens(); + void testNoWindowsAtStart(); + void testCreateWindow(); + void testHideShowCursor(); +}; + +void StartTest::initTestCase() +{ + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); +} + +void StartTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void StartTest::testScreens() +{ + QCOMPARE(screens()->count(), 1); + QCOMPARE(screens()->size(), QSize(1280, 1024)); + QCOMPARE(screens()->geometry(), QRect(0, 0, 1280, 1024)); +} + +void StartTest::testNoWindowsAtStart() +{ + QVERIFY(workspace()->clientList().isEmpty()); + QVERIFY(workspace()->desktopList().isEmpty()); + QVERIFY(workspace()->allClientList().isEmpty()); + QVERIFY(workspace()->deletedList().isEmpty()); + QVERIFY(workspace()->unmanagedList().isEmpty()); + QVERIFY(waylandServer()->clients().isEmpty()); +} + +void StartTest::testCreateWindow() +{ + // first we need to connect to the server + using namespace KWayland::Client; + QVERIFY(Test::setupWaylandConnection()); + + QSignalSpy shellClientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(shellClientAddedSpy.isValid()); + QSignalSpy shellClientRemovedSpy(waylandServer(), &WaylandServer::shellClientRemoved); + QVERIFY(shellClientRemovedSpy.isValid()); + + { + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + QSignalSpy surfaceRenderedSpy(surface.data(), &Surface::frameRendered); + QVERIFY(surfaceRenderedSpy.isValid()); + + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + Test::flushWaylandConnection(); + QVERIFY(waylandServer()->clients().isEmpty()); + // now dispatch should give us the client + waylandServer()->dispatch(); + QTRY_COMPARE(waylandServer()->clients().count(), 1); + // but still not yet in workspace + QVERIFY(workspace()->allClientList().isEmpty()); + + // icon geometry accesses windowManagementInterface which only exists after window became visible + // verify that accessing doesnt't crash + QVERIFY(waylandServer()->clients().first()->iconGeometry().isNull()); + + // let's render + Test::render(surface.data(), QSize(100, 50), Qt::blue); + surface->commit(); + + Test::flushWaylandConnection(); + QVERIFY(shellClientAddedSpy.wait()); + QCOMPARE(workspace()->allClientList().count(), 1); + QCOMPARE(workspace()->allClientList().first(), waylandServer()->clients().first()); + QVERIFY(workspace()->activeClient()); + QCOMPARE(workspace()->activeClient()->pos(), QPoint(0, 0)); + QCOMPARE(workspace()->activeClient()->size(), QSize(100, 50)); + QCOMPARE(workspace()->activeClient()->geometry(), QRect(0, 0, 100, 50)); + + // and kwin will render it + QVERIFY(surfaceRenderedSpy.wait()); + } + // this should tear down everything again + QVERIFY(shellClientRemovedSpy.wait()); + QVERIFY(waylandServer()->clients().isEmpty()); +} + + +void StartTest::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::StartTest) +#include "start_test.moc" diff --git a/autotests/integration/struts_test.cpp b/autotests/integration/struts_test.cpp new file mode 100644 index 0000000..b1cf00f --- /dev/null +++ b/autotests/integration/struts_test.cpp @@ -0,0 +1,962 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.h" +#include + +#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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + // 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(workspaceCreatedSpy.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); + Cursor::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); + ShellSurface *shellSurface = Test::createShellSurface(surface, surface); + Q_UNUSED(shellSurface) + PlasmaShellSurface *plasmaSurface = m_plasmaShell->createSurface(surface, surface); + plasmaSurface->setPosition(windowGeometry.topLeft()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + + // map the window + auto c = Test::renderAndWaitForShown(surface, windowGeometry.size(), Qt::red, QImage::Format_RGB32); + + QVERIFY(c); + QVERIFY(!c->isActive()); + QCOMPARE(c->geometry(), 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::createShellSurface(surface.data())); + Q_UNUSED(shellSurface) + QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); + plasmaSurface->setPosition(windowGeometry.topLeft()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + + // map the window + auto c = Test::renderAndWaitForShown(surface.data(), windowGeometry.size(), Qt::red, QImage::Format_RGB32); + QVERIFY(c); + QVERIFY(!c->isActive()); + QCOMPARE(c->geometry(), 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 geometryChangedSpy(c, &ShellClient::geometryShapeChanged); + QVERIFY(geometryChangedSpy.isValid()); + plasmaSurface->setPosition(QPoint(1280, 1000)); + QVERIFY(geometryChangedSpy.wait()); + QCOMPARE(c->geometry(), 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::createShellSurface(surface.data())); + Q_UNUSED(shellSurface) + QScopedPointer plasmaSurface(m_plasmaShell->createSurface(surface.data())); + plasmaSurface->setPosition(windowGeometry.topLeft()); + plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); + + // map the first panel + auto c = Test::renderAndWaitForShown(surface.data(), windowGeometry.size(), Qt::red, QImage::Format_RGB32); + QVERIFY(c); + QVERIFY(!c->isActive()); + QCOMPARE(c->geometry(), 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::createShellSurface(surface2.data())); + Q_UNUSED(shellSurface2) + QScopedPointer plasmaSurface2(m_plasmaShell->createSurface(surface2.data())); + plasmaSurface2->setPosition(windowGeometry2.topLeft()); + plasmaSurface2->setRole(PlasmaShellSurface::Role::Panel); + + auto c1 = Test::renderAndWaitForShown(surface2.data(), windowGeometry2.size(), Qt::blue, QImage::Format_RGB32); + + QVERIFY(c1); + QVERIFY(!c1->isActive()); + QCOMPARE(c1->geometry(), 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)); +} + +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()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->windowType(), NET::Dock); + QCOMPARE(client->geometry(), 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, &Client::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()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->windowType(), NET::Dock); + QCOMPARE(client->geometry(), 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, &Client::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()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->windowType(), NET::Dock); + QCOMPARE(client->geometry(), 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()); + Client *client2 = windowCreatedSpy.last().first().value(); + QVERIFY(client2); + QVERIFY(client2 != client); + QVERIFY(client2->isDecorated()); + QCOMPARE(client2->geometry(), QRect(0, 306, 1366, 744)); + QCOMPARE(client2->maximizeMode(), KWin::MaximizeFull); + // destroy window again + QSignalSpy normalWindowClosedSpy(client2, &Client::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, &Client::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()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->windowType(), NET::Dock); + QCOMPARE(client->geometry(), 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()); + Client *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->geometry(); + Cursor::setPos(origGeo.center()); + workspace()->performWindowOperation(client2, Options::MoveOp); + QTRY_COMPARE(workspace()->getMovingClient(), 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()->getMovingClient() == nullptr); + QCOMPARE(client2->geometry(), 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..627447f --- /dev/null +++ b/autotests/integration/tabbox_test.cpp @@ -0,0 +1,256 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "cursor.h" +#include "input.h" +#include "platform.h" +#include "screens.h" +#include "shell_client.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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.wait()); + waylandServer()->initWorkspace(); +} + +void TabBoxTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); + screens()->setCurrent(0); + KWin::Cursor::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::createShellSurface(Test::ShellSurfaceType::WlShell, 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::createShellSurface(Test::ShellSurfaceType::XdgShellV5, 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::createShellSurface(Test::ShellSurfaceType::XdgShellV5, 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::createShellSurface(Test::ShellSurfaceType::WlShell, 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::createShellSurface(Test::ShellSurfaceType::XdgShellV5, 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::createShellSurface(Test::ShellSurfaceType::XdgShellV5, 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::createShellSurface(Test::ShellSurfaceType::WlShell, 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::createShellSurface(Test::ShellSurfaceType::XdgShellV5, 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::createShellSurface(Test::ShellSurfaceType::XdgShellV5, 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..7df2970 --- /dev/null +++ b/autotests/integration/test_helpers.cpp @@ -0,0 +1,555 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "shell_client.h" +#include "screenlockerwatcher.h" +#include "wayland_server.h" + +#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 +{ + +static struct { + ConnectionThread *connection = nullptr; + EventQueue *queue = nullptr; + Compositor *compositor = nullptr; + ServerSideDecorationManager *decoration = nullptr; + ShadowManager *shadowManager = nullptr; + Shell *shell = nullptr; + XdgShell *xdgShellV5 = nullptr; + XdgShell *xdgShellV6 = nullptr; + ShmPool *shm = nullptr; + Seat *seat = nullptr; + PlasmaShell *plasmaShell = nullptr; + PlasmaWindowManagement *windowManagement = nullptr; + PointerConstraints *pointerConstraints = nullptr; + Registry *registry = nullptr; + QThread *thread = nullptr; + QVector outputs; + IdleInhibitManager *idleInhibit = nullptr; + AppMenuManager *appMenu = nullptr; +} s_waylandConnection; + +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); + }); + }); + + 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.shm = registry->createShmPool(registry->interface(Registry::Interface::Shm).name, registry->interface(Registry::Interface::Shm).version); + if (!s_waylandConnection.shm->isValid()) { + return false; + } + s_waylandConnection.shell = registry->createShell(registry->interface(Registry::Interface::Shell).name, registry->interface(Registry::Interface::Shell).version); + if (!s_waylandConnection.shell->isValid()) { + return false; + } + s_waylandConnection.xdgShellV5 = registry->createXdgShell(registry->interface(Registry::Interface::XdgShellUnstableV5).name, registry->interface(Registry::Interface::XdgShellUnstableV5).version); + if (!s_waylandConnection.xdgShellV5->isValid()) { + return false; + } + s_waylandConnection.xdgShellV6 = registry->createXdgShell(registry->interface(Registry::Interface::XdgShellUnstableV6).name, registry->interface(Registry::Interface::XdgShellUnstableV6).version); + if (!s_waylandConnection.xdgShellV6->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::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; + } + } + + return true; +} + +void destroyWaylandConnection() +{ + delete s_waylandConnection.compositor; + s_waylandConnection.compositor = 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.xdgShellV5; + s_waylandConnection.xdgShellV5 = nullptr; + delete s_waylandConnection.xdgShellV6; + s_waylandConnection.xdgShellV6 = nullptr; + delete s_waylandConnection.shell; + s_waylandConnection.shell = 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; + 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; +} + +Compositor *waylandCompositor() +{ + return s_waylandConnection.compositor; +} + +ShadowManager *waylandShadowManager() +{ + return s_waylandConnection.shadowManager; +} + +Shell *waylandShell() +{ + return s_waylandConnection.shell; +} + +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; +} + +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); +} + +ShellClient *waitForWaylandWindowShown(int timeout) +{ + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + if (!clientAddedSpy.isValid()) { + return nullptr; + } + if (!clientAddedSpy.wait(timeout)) { + return nullptr; + } + return clientAddedSpy.first().first().value(); +} + +ShellClient *renderAndWaitForShown(Surface *surface, const QSize &size, const QColor &color, const QImage::Format &format, int timeout) +{ + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + 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; +} + +ShellSurface *createShellSurface(Surface *surface, QObject *parent) +{ + if (!s_waylandConnection.shell) { + return nullptr; + } + auto s = s_waylandConnection.shell->createSurface(surface, parent); + if (!s->isValid()) { + delete s; + return nullptr; + } + return s; +} + +XdgShellSurface *createXdgShellV5Surface(Surface *surface, QObject *parent) +{ + if (!s_waylandConnection.xdgShellV5) { + return nullptr; + } + auto s = s_waylandConnection.xdgShellV5->createSurface(surface, parent); + if (!s->isValid()) { + delete s; + return nullptr; + } + return s; +} + +XdgShellSurface *createXdgShellV6Surface(Surface *surface, QObject *parent) +{ + if (!s_waylandConnection.xdgShellV6) { + return nullptr; + } + auto s = s_waylandConnection.xdgShellV6->createSurface(surface, parent); + if (!s->isValid()) { + delete s; + return nullptr; + } + return s; +} + +QObject *createShellSurface(ShellSurfaceType type, KWayland::Client::Surface *surface, QObject *parent) +{ + switch (type) { + case ShellSurfaceType::WlShell: + return createShellSurface(surface, parent); + case ShellSurfaceType::XdgShellV5: + return createXdgShellV5Surface(surface, parent); + case ShellSurfaceType::XdgShellV6: + return createXdgShellV6Surface(surface, parent); + default: + Q_UNREACHABLE(); + return nullptr; + } +} + +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..aee34c3 --- /dev/null +++ b/autotests/integration/touch_input_test.cpp @@ -0,0 +1,260 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "cursor.h" +#include "shell_client.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_touch_input-0"); + +class TouchInputTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + 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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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); + Cursor::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); + ShellSurface *shellSurface = Test::createShellSurface(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::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->geometry().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_no_input_test.cpp b/autotests/integration/transient_no_input_test.cpp new file mode 100644 index 0000000..62c58d5 --- /dev/null +++ b/autotests/integration/transient_no_input_test.cpp @@ -0,0 +1,116 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "abstract_client.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.h" + +#include +#include +#include +#include + +namespace KWin +{ + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_transient_no_input-0"); + +class TransientNoInputTest : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + void testTransientNoFocus(); +}; + +void TransientNoInputTest::initTestCase() +{ + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.wait()); +} + +void TransientNoInputTest::init() +{ + QVERIFY(Test::setupWaylandConnection()); +} + +void TransientNoInputTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +void TransientNoInputTest::testTransientNoFocus() +{ + using namespace KWayland::Client; + + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + + QScopedPointer surface(Test::createSurface()); + QVERIFY(!surface.isNull()); + + QScopedPointer shellSurface(Test::createShellSurface(surface.data())); + QVERIFY(!shellSurface.isNull()); + // let's render + Test::render(surface.data(), QSize(100, 50), Qt::blue); + + Test::flushWaylandConnection(); + QVERIFY(clientAddedSpy.wait()); + AbstractClient *c = workspace()->activeClient(); + QVERIFY(c); + QCOMPARE(clientAddedSpy.first().first().value(), c); + + // let's create a transient with no input + QScopedPointer transientSurface(Test::createSurface()); + QVERIFY(!transientSurface.isNull()); + QScopedPointer transientShellSurface(Test::createShellSurface(transientSurface.data())); + QVERIFY(!transientShellSurface.isNull()); + transientShellSurface->setTransient(surface.data(), QPoint(10, 20), ShellSurface::TransientFlag::NoFocus); + Test::flushWaylandConnection(); + // let's render + Test::render(transientSurface.data(), QSize(200, 20), Qt::red); + Test::flushWaylandConnection(); + QVERIFY(clientAddedSpy.wait()); + // get the latest ShellClient + auto transientClient = clientAddedSpy.last().first().value(); + QVERIFY(transientClient != c); + QCOMPARE(transientClient->geometry(), QRect(c->x() + 10, c->y() + 20, 200, 20)); + QVERIFY(transientClient->isTransient()); + QCOMPARE(transientClient->transientPlacementHint(), QPoint(10, 20)); + QVERIFY(!transientClient->wantsInput()); + + // workspace's active window should not have changed + QCOMPARE(workspace()->activeClient(), c); +} + +} + +WAYLANDTEST_MAIN(KWin::TransientNoInputTest) +#include "transient_no_input_test.moc" diff --git a/autotests/integration/transient_placement.cpp b/autotests/integration/transient_placement.cpp new file mode 100644 index 0000000..ac8869a --- /dev/null +++ b/autotests/integration/transient_placement.cpp @@ -0,0 +1,225 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 "shell_client.h" +#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 testSimplePosition_data(); + void testSimplePosition(); + void testDecorationPosition_data(); + void testDecorationPosition(); + +private: + AbstractClient *showWindow(const QSize &size, bool decorated = false, KWayland::Client::Surface *parent = nullptr, const QPoint &offset = QPoint()); + KWayland::Client::Surface *surfaceForClient(AbstractClient *c) const; +}; + +void TransientPlacementTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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)); + + screens()->setCurrent(0); + Cursor::setPos(QPoint(640, 512)); +} + +void TransientPlacementTest::cleanup() +{ + Test::destroyWaylandConnection(); +} + +AbstractClient *TransientPlacementTest::showWindow(const QSize &size, bool decorated, KWayland::Client::Surface *parent, const QPoint &offset) +{ + 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); + ShellSurface *shellSurface = Test::createShellSurface(surface, surface); + VERIFY(shellSurface); + if (parent) { + shellSurface->setTransient(parent, offset); + } + 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, size, Qt::blue); + + VERIFY(c); + COMPARE(workspace()->activeClient(), c); + +#undef VERIFY +#undef COMPARE + + return c; +} + +KWayland::Client::Surface *TransientPlacementTest::surfaceForClient(AbstractClient *c) const +{ + const auto &surfaces = KWayland::Client::Surface::all(); + auto it = std::find_if(surfaces.begin(), surfaces.end(), [c] (KWayland::Client::Surface *s) { return s->id() == c->surface()->id(); }); + if (it != surfaces.end()) { + return *it; + } + return nullptr; +} + +void TransientPlacementTest::testSimplePosition_data() +{ + QTest::addColumn("parentSize"); + QTest::addColumn("parentPosition"); + QTest::addColumn("transientSize"); + QTest::addColumn("transientOffset"); + QTest::addColumn("expectedGeometry"); + + QTest::newRow("0/0") << QSize(640, 512) << QPoint(0, 0) << QSize(10, 100) << QPoint(0, 0) << QRect(0, 0, 10, 100); + QTest::newRow("bottomRight") << QSize(640, 512) << QPoint(0, 0) << QSize(10, 100) << QPoint(639, 511) << QRect(639, 511, 10, 100); + QTest::newRow("offset") << QSize(640, 512) << QPoint(200, 300) << QSize(100, 10) << QPoint(320, 256) << QRect(520, 556, 100, 10); + QTest::newRow("right border") << QSize(1280, 1024) << QPoint(0, 0) << QSize(10, 100) << QPoint(1279, 50) << QRect(1269, 50, 10, 100); + QTest::newRow("bottom border") << QSize(1280, 1024) << QPoint(0, 0) << QSize(10, 100) << QPoint(512, 1020) << QRect(512, 920, 10, 100); + QTest::newRow("bottom right") << QSize(1280, 1024) << QPoint(0, 0) << QSize(10, 100) << QPoint(1279, 1020) << QRect(1269, 920, 10, 100); + QTest::newRow("top border") << QSize(1280, 1024) << QPoint(0, -100) << QSize(10, 100) << QPoint(512, 50) << QRect(512, 0, 10, 100); + QTest::newRow("left border") << QSize(1280, 1024) << QPoint(-100, 0) << QSize(100, 10) << QPoint(50, 512) << QRect(0, 512, 100, 10); + QTest::newRow("top left") << QSize(1280, 1024) << QPoint(-100, -100) << QSize(100, 100) << QPoint(50, 50) << QRect(0, 0, 100, 100); + QTest::newRow("bottom left") << QSize(1280, 1024) << QPoint(-100, 0) << QSize(100, 100) << QPoint(50, 1000) << QRect(0, 900, 100, 100); +} + +void TransientPlacementTest::testSimplePosition() +{ + // 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); + AbstractClient *parent = showWindow(parentSize); + QVERIFY(parent->clientPos().isNull()); + QVERIFY(!parent->isDecorated()); + QFETCH(QPoint, parentPosition); + parent->move(parentPosition); + QFETCH(QSize, transientSize); + QFETCH(QPoint, transientOffset); + AbstractClient *transient = showWindow(transientSize, false, surfaceForClient(parent), transientOffset); + QVERIFY(transient); + QVERIFY(!transient->isDecorated()); + QVERIFY(transient->hasTransientPlacementHint()); + QTEST(transient->geometry(), "expectedGeometry"); +} + +void TransientPlacementTest::testDecorationPosition_data() +{ + QTest::addColumn("parentSize"); + QTest::addColumn("parentPosition"); + QTest::addColumn("transientSize"); + QTest::addColumn("transientOffset"); + QTest::addColumn("expectedGeometry"); + + QTest::newRow("0/0") << QSize(640, 512) << QPoint(0, 0) << QSize(10, 100) << QPoint(0, 0) << QRect(0, 0, 10, 100); + QTest::newRow("bottomRight") << QSize(640, 512) << QPoint(0, 0) << QSize(10, 100) << QPoint(639, 511) << QRect(639, 511, 10, 100); + QTest::newRow("offset") << QSize(640, 512) << QPoint(200, 300) << QSize(100, 10) << QPoint(320, 256) << QRect(520, 556, 100, 10); +} + +void TransientPlacementTest::testDecorationPosition() +{ + // this test verifies that a transient window is correctly placed if the parent window has a + // server side decoration + QFETCH(QSize, parentSize); + AbstractClient *parent = showWindow(parentSize, true); + QVERIFY(!parent->clientPos().isNull()); + QVERIFY(parent->isDecorated()); + QFETCH(QPoint, parentPosition); + parent->move(parentPosition); + QFETCH(QSize, transientSize); + QFETCH(QPoint, transientOffset); + AbstractClient *transient = showWindow(transientSize, false, surfaceForClient(parent), transientOffset); + QVERIFY(transient); + QVERIFY(!transient->isDecorated()); + QVERIFY(transient->hasTransientPlacementHint()); + QFETCH(QRect, expectedGeometry); + expectedGeometry.translate(parent->clientPos()); + QCOMPARE(transient->geometry(), expectedGeometry); +} + +} + +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..71a3ec0 --- /dev/null +++ b/autotests/integration/virtual_desktop_test.cpp @@ -0,0 +1,169 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "main.h" +#include "platform.h" +#include "screens.h" +#include "shell_client.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_data(); + void testLastDesktopRemoved(); +}; + +void VirtualDesktopTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.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(workspaceCreatedSpy.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_data() +{ + QTest::addColumn("type"); + + QTest::newRow("wlShell") << Test::ShellSurfaceType::WlShell; + QTest::newRow("xdgShellV5") << Test::ShellSurfaceType::XdgShellV5; + QTest::newRow("xdgShellV6") << Test::ShellSurfaceType::XdgShellV6; +} + +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()); + QFETCH(Test::ShellSurfaceType, type); + QScopedPointer shellSurface(Test::createShellSurface(type, surface.data())); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + + QVERIFY(client); + QCOMPARE(client->desktop(), 2); + QSignalSpy desktopPresenceChangedSpy(client, &ShellClient::desktopPresenceChanged); + QVERIFY(desktopPresenceChangedSpy.isValid()); + + // 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); +} + +WAYLANDTEST_MAIN(VirtualDesktopTest) +#include "virtual_desktop_test.moc" diff --git a/autotests/integration/window_rules_test.cpp b/autotests/integration/window_rules_test.cpp new file mode 100644 index 0000000..2bd14ed --- /dev/null +++ b/autotests/integration/window_rules_test.cpp @@ -0,0 +1,171 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "atoms.h" +#include "client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "rules.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.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 WindowRuleTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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); + Cursor::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()); + Client *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, &Client::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..3b70a37 --- /dev/null +++ b/autotests/integration/window_selection_test.cpp @@ -0,0 +1,559 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "cursor.h" +#include "keyboard_input.h" +#include "platform.h" +#include "pointer_input.h" +#include "shell_client.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_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(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + qputenv("XKB_DEFAULT_RULES", "evdev"); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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::Cursor::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::createShellSurface(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::Cursor::setPos(client->geometry().center()); + QCOMPARE(input()->pointer()->window().data(), 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()->window().isNull()); + + // updating the pointer should not change anything + input()->pointer()->update(); + QVERIFY(input()->pointer()->window().isNull()); + // 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()->window().data(), 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::createShellSurface(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->geometry().contains(KWin::Cursor::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::Cursor::pos().x() >= client->geometry().x() + client->geometry().width()) { + keyPress(KEY_LEFT); + } + while (KWin::Cursor::pos().x() <= client->geometry().x()) { + keyPress(KEY_RIGHT); + } + while (KWin::Cursor::pos().y() <= client->geometry().y()) { + keyPress(KEY_DOWN); + } + while (KWin::Cursor::pos().y() >= client->geometry().y() + client->geometry().height()) { + keyPress(KEY_UP); + } + QFETCH(qint32, key); + kwinApp()->platform()->keyboardKeyPressed(key, timestamp++); + QCOMPARE(input()->isSelectingWindow(), false); + QCOMPARE(selectedWindow, client); + QCOMPARE(input()->pointer()->window().data(), 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::createShellSurface(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->geometry().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->geometry().bottomRight() + QPoint(20, 20), timestamp++); + QVERIFY(!selectedWindow); + kwinApp()->platform()->touchMotion(0, client->geometry().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->geometry().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->geometry().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::createShellSurface(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::Cursor::setPos(client->geometry().center()); + QCOMPARE(input()->pointer()->window().data(), 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()->window().data(), 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::createShellSurface(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::Cursor::setPos(client->geometry().center()); + QCOMPARE(input()->pointer()->window().data(), 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()->window().data(), 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::createShellSurface(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::Cursor::setPos(client->geometry().center()); + QCOMPARE(input()->pointer()->window().data(), 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()->window().isNull()); + + // updating the pointer should not change anything + input()->pointer()->update(); + QVERIFY(input()->pointer()->window().isNull()); + // 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()->window().data(), 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..7be40fe --- /dev/null +++ b/autotests/integration/x11_client_test.cpp @@ -0,0 +1,605 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "atoms.h" +#include "client.h" +#include "composite.h" +#include "effects.h" +#include "effectloader.h" +#include "cursor.h" +#include "platform.h" +#include "screens.h" +#include "shell_client.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_x11_client-0"); + +class X11ClientTest : public QObject +{ +Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void init(); + void cleanup(); + + void testCaptionSimplified(); + void testFullscreenLayerWithActiveWaylandWindow(); + void testFocusInWithWaylandLastActiveWindow(); + void testX11WindowId(); + void testCaptionChanges(); + void testCaptionWmName(); + void testCaptionMultipleWindows(); + void testFullscreenWindowGroups(); +}; + +void X11ClientTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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::testCaptionSimplified() +{ + // this test verifies that caption is properly trimmed + // see BUG 323798 comment #12 + 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()); + const QByteArray origTitle = QByteArrayLiteral("Was tun, wenn Schüler Autismus haben?\342\200\250\342\200\250\342\200\250 – Marlies Hübner - Mozilla Firefox"); + winInfo.setName(origTitle.constData()); + 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()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->caption() != QString::fromUtf8(origTitle)); + QCOMPARE(client->caption(), QString::fromUtf8(origTitle).simplified()); + + // and destroy the window again + xcb_unmap_window(c.data(), w); + xcb_flush(c.data()); + + QSignalSpy windowClosedSpy(client, &Client::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()); + Client *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::createShellSurface(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()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->window(), w); + QVERIFY(client->isActive()); + + // create Wayland window + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(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()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->windowId(), w); + QVERIFY(client->isActive()); + QCOMPARE(client->window(), w); + + NETRootInfo rootInfo(c.data(), NET::WMAllProperties); + QCOMPARE(rootInfo.activeWindow(), client->window()); + + // activate a wayland window + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface(Test::createShellSurface(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()); +} + +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()); + Client *client = windowCreatedSpy.first().first().value(); + QVERIFY(client); + QCOMPARE(client->windowId(), w); + QCOMPARE(client->caption(), QStringLiteral("foo")); + + QSignalSpy captionChangedSpy(client, &Client::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, &Client::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.start(QStringLiteral("glxgears")); + QVERIFY(glxgears.waitForStarted()); + + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + QCOMPARE(workspace()->clientList().count(), 1); + Client *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()); + Client *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()); + Client *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, &Client::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()); + Client *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()); + Client *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); +} + +WAYLANDTEST_MAIN(X11ClientTest) +#include "x11_client_test.moc" diff --git a/autotests/integration/xclipboardsync_test.cpp b/autotests/integration/xclipboardsync_test.cpp new file mode 100644 index 0000000..ee9a5e6 --- /dev/null +++ b/autotests/integration/xclipboardsync_test.cpp @@ -0,0 +1,183 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "shell_client.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include + +#include +#include + +using namespace KWin; + +static const QString s_socketName = QStringLiteral("wayland_test_kwin_xclipboard_sync-0"); + +class XClipboardSyncTest : 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 XClipboardSyncTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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 + while (waylandServer()->xclipboardSyncDataDevice().isNull()) { + QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents); + } + QVERIFY(!waylandServer()->xclipboardSyncDataDevice().isNull()); +} + +void XClipboardSyncTest::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 XClipboardSyncTest::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 XClipboardSyncTest::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 shellClientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(shellClientAddedSpy.isValid()); + QSignalSpy clipboardChangedSpy(waylandServer()->xclipboardSyncDataDevice().data(), &KWayland::Server::DataDeviceInterface::selectionChanged); + QVERIFY(clipboardChangedSpy.isValid()); + + QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); + 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; + if (copyPlatform == QLatin1String("xcb")) { + QVERIFY(clientAddedSpy.wait()); + copyClient = clientAddedSpy.first().first().value(); + } else { + QVERIFY(shellClientAddedSpy.wait()); + copyClient = shellClientAddedSpy.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; + if (pastePlatform == QLatin1String("xcb")) { + QVERIFY(clientAddedSpy.wait()); + pasteClient = clientAddedSpy.last().first().value(); + } else { + QVERIFY(shellClientAddedSpy.wait()); + pasteClient = shellClientAddedSpy.last().first().value(); + } + QCOMPARE(clientAddedSpy.count(), 1); + QCOMPARE(shellClientAddedSpy.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; +} + +WAYLANDTEST_MAIN(XClipboardSyncTest) +#include "xclipboardsync_test.moc" diff --git a/autotests/integration/xwayland_input_test.cpp b/autotests/integration/xwayland_input_test.cpp new file mode 100644 index 0000000..a88bc3a --- /dev/null +++ b/autotests/integration/xwayland_input_test.cpp @@ -0,0 +1,212 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "kwin_wayland_test.h" +#include "platform.h" +#include "client.h" +#include "cursor.h" +#include "deleted.h" +#include "screenedge.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" +#include "shell_client.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 testPointerEnterLeave(); +}; + +void XWaylandInputTest::initTestCase() +{ + qRegisterMetaType(); + qRegisterMetaType(); + qRegisterMetaType(); + QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); + QVERIFY(workspaceCreatedSpy.isValid()); + kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); + QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); + QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); + + kwinApp()->start(); + QVERIFY(workspaceCreatedSpy.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); + Cursor::setPos(QPoint(640, 512)); + 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(); + void left(); + +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: + emit entered(); + break; + case XCB_LEAVE_NOTIFY: + emit left(); + break; + } + free(event); + } + xcb_flush(m_connection); +} + +void XWaylandInputTest::testPointerEnterLeave() +{ + // this test simulates a pointer enter and pointer leave on an 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, 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()); + Client *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->geometry().contains(Cursor::pos())); + QVERIFY(enteredSpy.isEmpty()); + Cursor::setPos(client->geometry().center()); + QCOMPARE(waylandServer()->seat()->focusedPointerSurface(), client->surface()); + QVERIFY(waylandServer()->seat()->focusedPointer()); + QVERIFY(enteredSpy.wait()); + + // move out of window + Cursor::setPos(client->geometry().bottomRight() + QPoint(10, 10)); + QVERIFY(leftSpy.wait()); + + // destroy window again + QSignalSpy windowClosedSpy(client, &Client::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::XWaylandInputTest) +#include "xwayland_input_test.moc" diff --git a/autotests/libinput/CMakeLists.txt b/autotests/libinput/CMakeLists.txt new file mode 100644 index 0000000..bc0fff6 --- /dev/null +++ b/autotests/libinput/CMakeLists.txt @@ -0,0 +1,113 @@ +include_directories(${Libinput_INCLUDE_DIRS}) +include_directories(${UDEV_INCLUDE_DIR}) +######################################################## +# Test Devices +######################################################## +set( testLibinputDevice_SRCS device_test.cpp mock_libinput.cpp ../../libinput/device.cpp ) +add_executable(testLibinputDevice ${testLibinputDevice_SRCS}) +target_link_libraries( testLibinputDevice Qt5::Test Qt5::DBus Qt5::Gui KF5::ConfigCore) +add_test(NAME kwin-testLibinputDevice COMMAND testLibinputDevice) +ecm_mark_as_test(testLibinputDevice) + +######################################################## +# Test Key Event +######################################################## +set( testLibinputKeyEvent_SRCS + key_event_test.cpp + mock_libinput.cpp + ../../libinput/device.cpp + ../../libinput/events.cpp + ) +add_executable(testLibinputKeyEvent ${testLibinputKeyEvent_SRCS}) +target_link_libraries( testLibinputKeyEvent Qt5::Test Qt5::DBus Qt5::Widgets KF5::ConfigCore) +add_test(NAME kwin-testLibinputKeyEvent COMMAND testLibinputKeyEvent) +ecm_mark_as_test(testLibinputKeyEvent) + +######################################################## +# Test Pointer Event +######################################################## +set( testLibinputPointerEvent_SRCS + pointer_event_test.cpp + mock_libinput.cpp + ../../libinput/device.cpp + ../../libinput/events.cpp + ) +add_executable(testLibinputPointerEvent ${testLibinputPointerEvent_SRCS}) +target_link_libraries( testLibinputPointerEvent Qt5::Test Qt5::DBus Qt5::Widgets KF5::ConfigCore) +add_test(NAME kwin-testLibinputPointerEvent COMMAND testLibinputPointerEvent) +ecm_mark_as_test(testLibinputPointerEvent) + +######################################################## +# Test Touch Event +######################################################## +set( testLibinputTouchEvent_SRCS + touch_event_test.cpp + mock_libinput.cpp + ../../libinput/device.cpp + ../../libinput/events.cpp + ) +add_executable(testLibinputTouchEvent ${testLibinputTouchEvent_SRCS}) +target_link_libraries( testLibinputTouchEvent Qt5::Test Qt5::DBus Qt5::Widgets KF5::ConfigCore) +add_test(NAME kwin-testLibinputTouchEvent COMMAND testLibinputTouchEvent) +ecm_mark_as_test(testLibinputTouchEvent) + +######################################################## +# Test Gesture Event +######################################################## +set( testLibinputGestureEvent_SRCS + gesture_event_test.cpp + mock_libinput.cpp + ../../libinput/device.cpp + ../../libinput/events.cpp + ) +add_executable(testLibinputGestureEvent ${testLibinputGestureEvent_SRCS}) +target_link_libraries( testLibinputGestureEvent Qt5::Test Qt5::DBus Qt5::Widgets KF5::ConfigCore) +add_test(NAME kwin-testLibinputGestureEvent COMMAND testLibinputGestureEvent) +ecm_mark_as_test(testLibinputGestureEvent) + +######################################################## +# Test Switch Event +######################################################## +set( testLibinputSwitchEvent_SRCS + switch_event_test.cpp + mock_libinput.cpp + ../../libinput/device.cpp + ../../libinput/events.cpp + ) +add_executable(testLibinputSwitchEvent ${testLibinputSwitchEvent_SRCS}) +target_link_libraries(testLibinputSwitchEvent Qt5::Test Qt5::DBus Qt5::Widgets KF5::ConfigCore) +add_test(NAME kwin-testLibinputSwitchEvent COMMAND testLibinputSwitchEvent) +ecm_mark_as_test(testLibinputSwitchEvent) + +######################################################## +# Test Context +######################################################## +set( testLibinputContext_SRCS + context_test.cpp + mock_libinput.cpp + mock_udev.cpp + ../../libinput/context.cpp + ../../libinput/device.cpp + ../../libinput/events.cpp + ../../libinput/libinput_logging.cpp + ../../logind.cpp + ) +add_executable(testLibinputContext ${testLibinputContext_SRCS}) +target_link_libraries( testLibinputContext + 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 +######################################################## +set( testInputEvents_SRCS input_event_test.cpp mock_libinput.cpp ../../libinput/device.cpp ../../input_event.cpp ) +add_executable(testInputEvents ${testInputEvents_SRCS}) +target_link_libraries( testInputEvents Qt5::Test Qt5::DBus Qt5::Gui KF5::ConfigCore) +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..a860bcd --- /dev/null +++ b/autotests/libinput/context_test.cpp @@ -0,0 +1,92 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..7a86776 --- /dev/null +++ b/autotests/libinput/device_test.cpp @@ -0,0 +1,2198 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "mock_libinput.h" +#include "../../libinput/device.h" +#include + +#include + +#include + +#include + +using namespace KWin::LibInput; + +class TestLibinputDevice : public QObject +{ + Q_OBJECT +private Q_SLOTS: + 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 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 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 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(); +}; + +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(d.isPointer(), pointer); + QCOMPARE(d.property("pointer").toBool(), pointer); + QCOMPARE(d.isTouch(), touch); + QCOMPARE(d.property("touch").toBool(), touch); + QCOMPARE(d.isTabletPad(), false); + QCOMPARE(d.property("tabletPad").toBool(), false); + QCOMPARE(d.isTabletTool(), tabletTool); + QCOMPARE(d.property("tabletTool").toBool(), tabletTool); + QCOMPARE(d.isSwitch(), switchDevice); + QCOMPARE(d.property("switchDevice").toBool(), 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); +} + +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(d.sysName().toUtf8(), sysName); + QCOMPARE(d.property("sysName").toString().toUtf8(), sysName); + QCOMPARE(d.outputName().toUtf8(), outputName); + QCOMPARE(d.property("outputName").toString().toUtf8(), 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); +} + +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); +} + +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); +} + +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"); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); + + QSignalSpy pointerAccelChangedSpy(&d, &Device::pointerAccelerationChanged); + QVERIFY(pointerAccelChangedSpy.isValid()); + QFETCH(qreal, setAccel); + d.setPointerAcceleration(setAccel); + QTEST(d.pointerAcceleration(), "expectedAccel"); + QTEST(!pointerAccelChangedSpy.isEmpty(), "expectedChanged"); +} + +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); + + 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); +} + +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"); +} + +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"); +} + + +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); + + QSignalSpy enabledChangedSpy(&d, &Device::enabledChanged); + QVERIFY(enabledChangedSpy.isValid()); + QFETCH(bool, setValue); + d.setEnabled(setValue); + QFETCH(bool, expectedValue); + QCOMPARE(d.isEnabled(), 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); + + 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); +} + +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); +} + +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); + + 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); +} + +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); +} + +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); + + 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); +} + +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); + + 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); +} + +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); + + 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); +} + +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); + + 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); +} + +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); + + 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); +} + +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); + + 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); +} + +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); + + 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); +} + +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); +} + +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); +} + +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); + + 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); +} + +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); + + // and try to store + if (configValue != initValue) { + d.setProperty(initValuePropName, true); + QCOMPARE(inputConfig.readEntry("PointerAccelerationProfile", 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); + + // set the group + d.setConfig(inputConfig); + d.loadConfiguration(); + QCOMPARE(d.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(d.isTabletModeSwitch(), tablet); + QCOMPARE(d.property("tabletModeSwitch").toBool(), 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..77c0499 --- /dev/null +++ b/autotests/libinput/gesture_event_test.cpp @@ -0,0 +1,214 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..b1a83aa --- /dev/null +++ b/autotests/libinput/input_event_test.cpp @@ -0,0 +1,184 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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("expectedAngleDelta"); + + QTest::newRow("horiz") << Qt::Horizontal << 3.0 << QPoint(3, 0); + QTest::newRow("vert") << Qt::Vertical << 2.0 << 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); + WheelEvent event(QPointF(100, 200), delta, orientation, Qt::LeftButton | Qt::RightButton, + Qt::ShiftModifier | Qt::ControlModifier, 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"); + // 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..97aea0c --- /dev/null +++ b/autotests/libinput/key_event_test.cpp @@ -0,0 +1,116 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..a412078 --- /dev/null +++ b/autotests/libinput/mock_libinput.cpp @@ -0,0 +1,858 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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_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; + } +} + +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; +} diff --git a/autotests/libinput/mock_libinput.h b/autotests/libinput/mock_libinput.h new file mode 100644 index 0000000..404b0d6 --- /dev/null +++ b/autotests/libinput/mock_libinput.h @@ -0,0 +1,159 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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; +}; + +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; + 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..fc8e228 --- /dev/null +++ b/autotests/libinput/mock_udev.cpp @@ -0,0 +1,37 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..bfd6455 --- /dev/null +++ b/autotests/libinput/mock_udev.h @@ -0,0 +1,28 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..bad8520 --- /dev/null +++ b/autotests/libinput/pointer_event_test.cpp @@ -0,0 +1,209 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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) + +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("time"); + + QTest::newRow("horizontal") << true << false << QPointF(3.0, 0.0) << 100u; + QTest::newRow("vertical") << false << true << QPointF(0.0, 2.5) << 200u; + QTest::newRow("both") << true << true << QPointF(1.1, 4.2) << 300u; +} + +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(quint32, time); + pointerEvent->horizontalAxis = horizontal; + pointerEvent->verticalAxis = vertical; + pointerEvent->horizontalAxisValue = value.x(); + pointerEvent->verticalAxisValue = value.y(); + 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->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..ea0b2e9 --- /dev/null +++ b/autotests/libinput/switch_event_test.cpp @@ -0,0 +1,99 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..53bae1e --- /dev/null +++ b/autotests/libinput/touch_event_test.cpp @@ -0,0 +1,145 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..e570fe3 --- /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=5 +ChipClass=999 +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..35e275d --- /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=5 +ChipClass=999 +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..a20045e --- /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=5 +ChipClass=999 +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-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/kwinglplatformtest.cpp b/autotests/libkwineffects/kwinglplatformtest.cpp new file mode 100644 index 0000000..2f069b8 --- /dev/null +++ b/autotests/libkwineffects/kwinglplatformtest.cpp @@ -0,0 +1,284 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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("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("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("NI"); + 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")); + + QEXPECT_FAIL("amd-gallium-bonaire-3.0", "Not detected as a radeon driver", Continue); + QEXPECT_FAIL("amd-gallium-hawaii-3.0", "Not detected as a radeon driver", Continue); + QEXPECT_FAIL("amd-gallium-tonga-4.1", "Not detected as a radeon driver", Continue); + QCOMPARE(gl->driver(), Driver(settingsGroup.readEntry("Driver", int(Driver_Unknown)))); + QEXPECT_FAIL("amd-gallium-bonaire-3.0", "Not detected as a radeon driver", Continue); + QEXPECT_FAIL("amd-gallium-hawaii-3.0", "Not detected as a radeon driver", Continue); + QEXPECT_FAIL("amd-gallium-tonga-4.1", "Not detected as a radeon driver", Continue); + QCOMPARE(gl->chipClass(), ChipClass(settingsGroup.readEntry("ChipClass", int(UnknownChipClass)))); + + QCOMPARE(gl->isMesaDriver(), settingsGroup.readEntry("Mesa", false)); + QCOMPARE(gl->isGalliumDriver(), settingsGroup.readEntry("Gallium", false)); + QEXPECT_FAIL("amd-gallium-bonaire-3.0", "Not detected as a radeon driver", Continue); + QEXPECT_FAIL("amd-gallium-hawaii-3.0", "Not detected as a radeon driver", Continue); + QEXPECT_FAIL("amd-gallium-tonga-4.1", "Not detected as a radeon driver", Continue); + 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->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..51fffbe --- /dev/null +++ b/autotests/libkwineffects/mock_gl.cpp @@ -0,0 +1,70 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..cd53320 --- /dev/null +++ b/autotests/libkwineffects/mock_gl.h @@ -0,0 +1,39 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..8483562 --- /dev/null +++ b/autotests/libkwineffects/timelinetest.cpp @@ -0,0 +1,256 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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 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()); +} + +QTEST_MAIN(TimeLineTest) + +#include "timelinetest.moc" diff --git a/autotests/libkwineffects/windowquadlisttest.cpp b/autotests/libkwineffects/windowquadlisttest.cpp new file mode 100644 index 0000000..a251929 --- /dev/null +++ b/autotests/libkwineffects/windowquadlisttest.cpp @@ -0,0 +1,221 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..062dc18 --- /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..697b12d --- /dev/null +++ b/autotests/libxrenderutils/blendpicture_test.cpp @@ -0,0 +1,61 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..d270f63 --- /dev/null +++ b/autotests/mock_abstract_client.cpp @@ -0,0 +1,117 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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_geometry() + , 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::setGeometry(const QRect &rect) +{ + m_geometry = rect; + emit geometryChanged(); +} + +QRect AbstractClient::geometry() const +{ + return m_geometry; +} + +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..8d7e825 --- /dev/null +++ b/autotests/mock_abstract_client.h @@ -0,0 +1,70 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~AbstractClient(); + + int screen() const; + bool isOnScreen(int screen) const; + bool isActive() const; + bool isFullScreen() const; + bool isHiddenInternal() const; + QRect geometry() const; + bool keepBelow() const; + + void setActive(bool active); + void setScreen(int screen); + void setFullScreen(bool set); + void setHiddenInternal(bool set); + void setGeometry(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_geometry; + bool m_resize; +}; + +} + +#endif diff --git a/autotests/mock_client.cpp b/autotests/mock_client.cpp new file mode 100644 index 0000000..88b5fa6 --- /dev/null +++ b/autotests/mock_client.cpp @@ -0,0 +1,38 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "mock_client.h" + +namespace KWin +{ + +Client::Client(QObject *parent) + : AbstractClient(parent) +{ +} + +Client::~Client() = default; + +void Client::showOnScreenEdge() +{ + setKeepBelow(false); + setHiddenInternal(false); +} + +} diff --git a/autotests/mock_client.h b/autotests/mock_client.h new file mode 100644 index 0000000..17db3c8 --- /dev/null +++ b/autotests/mock_client.h @@ -0,0 +1,43 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_MOCK_CLIENT_H +#define KWIN_MOCK_CLIENT_H + +#include + +#include +#include + +namespace KWin +{ + +class Client : public AbstractClient +{ + Q_OBJECT +public: + explicit Client(QObject *parent); + virtual ~Client(); + void showOnScreenEdge() override; + +}; + +} + +#endif diff --git a/autotests/mock_effectshandler.cpp b/autotests/mock_effectshandler.cpp new file mode 100644 index 0000000..be4b1ff --- /dev/null +++ b/autotests/mock_effectshandler.cpp @@ -0,0 +1,38 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..72351f2 --- /dev/null +++ b/autotests/mock_effectshandler.h @@ -0,0 +1,269 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 { + return nullptr; + } + 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, 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(KWayland::Server::SurfaceInterface *) const override { + 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 *, QRegion, double, double) override {} + void paintScreen(int, QRegion, KWin::ScreenPaintData &) override {} + void paintWindow(KWin::EffectWindow *, int, 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(); + } + KWayland::Server::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)} + + KSharedConfigPtr config() const override; + KSharedConfigPtr inputConfig() const override; + +private: + bool m_animationsSuported = true; +}; +#endif diff --git a/autotests/mock_screens.cpp b/autotests/mock_screens.cpp new file mode 100644 index 0000000..2124b38 --- /dev/null +++ b/autotests/mock_screens.cpp @@ -0,0 +1,98 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..5c6b8c2 --- /dev/null +++ b/autotests/mock_screens.h @@ -0,0 +1,53 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~MockScreens(); + 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..3f75e9e --- /dev/null +++ b/autotests/mock_workspace.cpp @@ -0,0 +1,90 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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_movingClient(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::getMovingClient() const +{ + return m_movingClient; +} + +void MockWorkspace::setMovingClient(AbstractClient *c) +{ + m_movingClient = 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..c04ef37 --- /dev/null +++ b/autotests/mock_workspace.h @@ -0,0 +1,83 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_MOCK_WORKSPACE_H +#define KWIN_MOCK_WORKSPACE_H + +#include +#include + +namespace KWin +{ + +class AbstractClient; +class Client; +class X11EventFilter; + +class MockWorkspace; +typedef MockWorkspace Workspace; + +class MockWorkspace : public QObject +{ + Q_OBJECT +public: + explicit MockWorkspace(QObject *parent = nullptr); + virtual ~MockWorkspace(); + AbstractClient *activeClient() const; + AbstractClient *getMovingClient() const; + void setShowingDesktop(bool showing); + bool showingDesktop() const; + QRect clientArea(clientAreaOption, int screen, int desktop) const; + + void setActiveClient(AbstractClient *c); + void setMovingClient(AbstractClient *c); + + void registerEventFilter(X11EventFilter *filter); + void unregisterEventFilter(X11EventFilter *filter); + + bool compositing() const { + return false; + } + + static Workspace *self(); + +Q_SIGNALS: + void clientRemoved(KWin::Client*); + +private: + AbstractClient *m_activeClient; + AbstractClient *m_movingClient; + 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/onscreennotificationtest.cpp b/autotests/onscreennotificationtest.cpp new file mode 100644 index 0000000..a46e851 --- /dev/null +++ b/autotests/onscreennotificationtest.cpp @@ -0,0 +1,141 @@ +/* + * Copyright 2016 Martin Graesslin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#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); +} + +#include "onscreennotificationtest.moc" diff --git a/autotests/onscreennotificationtest.h b/autotests/onscreennotificationtest.h new file mode 100644 index 0000000..6f525ad --- /dev/null +++ b/autotests/onscreennotificationtest.h @@ -0,0 +1,38 @@ +/* + * Copyright 2016 Martin Graesslin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#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..4450415 --- /dev/null +++ b/autotests/opengl_context_attribute_builder_test.cpp @@ -0,0 +1,442 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..38ad420 --- /dev/null +++ b/autotests/tabbox/CMakeLists.txt @@ -0,0 +1,94 @@ +include_directories(${KWIN_SOURCE_DIR}) +add_definitions(-DKWIN_UNIT_TEST) +######################################################## +# Test TabBox::ClientModel +######################################################## +set( testTabBoxClientModel_SRCS + ../../tabbox/clientmodel.cpp + ../../tabbox/desktopmodel.cpp + ../../tabbox/tabboxconfig.cpp + ../../tabbox/tabboxhandler.cpp + ../../tabbox/tabbox_logging.cpp + test_tabbox_clientmodel.cpp + mock_tabboxhandler.cpp + mock_tabboxclient.cpp +) + +add_executable( testTabBoxClientModel ${testTabBoxClientModel_SRCS} ) +set_target_properties(testTabBoxClientModel PROPERTIES COMPILE_DEFINITIONS "NO_NONE_WINDOW") +target_link_libraries( testTabBoxClientModel + Qt5::Core + Qt5::Widgets + Qt5::Script + Qt5::Quick + Qt5::Test + 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/tabboxconfig.cpp + ../../tabbox/tabboxhandler.cpp + ../../tabbox/tabbox_logging.cpp + test_tabbox_handler.cpp + mock_tabboxhandler.cpp + mock_tabboxclient.cpp +) + +add_executable( testTabBoxHandler ${testTabBoxHandler_SRCS} ) +set_target_properties(testTabBoxHandler PROPERTIES COMPILE_DEFINITIONS "NO_NONE_WINDOW") +target_link_libraries( testTabBoxHandler + Qt5::Core + Qt5::Widgets + Qt5::Script + Qt5::Quick + Qt5::Test + 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/tabboxconfig.cpp + ../../tabbox/tabbox_logging.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..5b5f09e --- /dev/null +++ b/autotests/tabbox/mock_tabboxclient.cpp @@ -0,0 +1,38 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "mock_tabboxclient.h" +#include "mock_tabboxhandler.h" + +namespace KWin +{ + +MockTabBoxClient::MockTabBoxClient(QString caption, WId id) + : TabBoxClient() + , m_caption(caption) + , m_wId(id) +{ +} + +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..32b42d7 --- /dev/null +++ b/autotests/tabbox/mock_tabboxclient.h @@ -0,0 +1,73 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_MOCK_TABBOX_CLIENT_H +#define KWIN_MOCK_TABBOX_CLIENT_H + +#include "../../tabbox/tabboxhandler.h" + +#include + +namespace KWin +{ +class MockTabBoxClient : public TabBox::TabBoxClient +{ +public: + explicit MockTabBoxClient(QString caption, WId id); + virtual bool isMinimized() const { + return false; + } + virtual QString caption() const { + return m_caption; + } + virtual void close(); + virtual int height() const { + return 100; + } + virtual QPixmap icon(const QSize &size = QSize(32, 32)) const { + return QPixmap(size); + } + virtual bool isCloseable() const { + return true; + } + virtual bool isFirstInTabBox() const { + return false; + } + virtual int width() const { + return 100; + } + virtual WId window() const { + return m_wId; + } + virtual int x() const { + return 0; + } + virtual int y() const { + return 0; + } + virtual QIcon icon() const { + return QIcon(); + } + +private: + QString m_caption; + WId m_wId; +}; +} // namespace KWin +#endif diff --git a/autotests/tabbox/mock_tabboxhandler.cpp b/autotests/tabbox/mock_tabboxhandler.cpp new file mode 100644 index 0000000..1e326e9 --- /dev/null +++ b/autotests/tabbox/mock_tabboxhandler.cpp @@ -0,0 +1,122 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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, WId id) +{ + QSharedPointer< TabBox::TabBoxClient > client(new MockTabBoxClient(caption, id)); + 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..4820a3d --- /dev/null +++ b/autotests/tabbox/mock_tabboxhandler.h @@ -0,0 +1,113 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~MockTabBoxHandler(); + virtual void activateAndClose() { + } + virtual QWeakPointer< TabBox::TabBoxClient > activeClient() const; + void setActiveClient(const QWeakPointer &client); + virtual int activeScreen() const { + return 0; + } + virtual QWeakPointer< TabBox::TabBoxClient > clientToAddToList(TabBox::TabBoxClient *client, int desktop) const; + virtual int currentDesktop() const { + return 1; + } + virtual QWeakPointer< TabBox::TabBoxClient > desktopClient() const { + return QWeakPointer(); + } + virtual QString desktopName(int desktop) const { + Q_UNUSED(desktop) + return "desktop 1"; + } + virtual QString desktopName(TabBox::TabBoxClient *client) const { + Q_UNUSED(client) + return "desktop"; + } + virtual void elevateClient(TabBox::TabBoxClient *c, QWindow *tabbox, bool elevate) const { + Q_UNUSED(c) + Q_UNUSED(tabbox) + Q_UNUSED(elevate) + } + virtual void shadeClient(TabBox::TabBoxClient *c, bool b) const { + Q_UNUSED(c) + Q_UNUSED(b) + } + virtual void hideOutline() { + } + virtual QWeakPointer< TabBox::TabBoxClient > nextClientFocusChain(TabBox::TabBoxClient *client) const; + virtual QWeakPointer firstClientFocusChain() const; + virtual bool isInFocusChain (TabBox::TabBoxClient* client) const; + virtual int nextDesktopFocusChain(int desktop) const { + Q_UNUSED(desktop) + return 1; + } + virtual int numberOfDesktops() const { + return 1; + } + virtual QVector< xcb_window_t > outlineWindowIds() const { + return QVector(); + } + virtual bool isKWinCompositing() const { + return false; + } + virtual void raiseClient(TabBox::TabBoxClient *c) const { + Q_UNUSED(c) + } + virtual void restack(TabBox::TabBoxClient *c, TabBox::TabBoxClient *under) { + Q_UNUSED(c) + Q_UNUSED(under) + } + virtual void showOutline(const QRect &outline) { + Q_UNUSED(outline) + } + virtual TabBox::TabBoxClientList stackingOrder() const { + return TabBox::TabBoxClientList(); + } + virtual void grabbedKeyEvent(QKeyEvent *event) const; + + 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, WId id); + 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..a504c55 --- /dev/null +++ b/autotests/tabbox/test_desktopchain.cpp @@ -0,0 +1,263 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ + +// 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..201f7be --- /dev/null +++ b/autotests/tabbox/test_tabbox_clientmodel.cpp @@ -0,0 +1,94 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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"), 1); + 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"), 1); + tabboxhandler.createMockWindow(QString("test2"), 2); + 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"), 1); + client = tabboxhandler.createMockWindow(QString("test2"), 2); + 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(client.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..35390a9 --- /dev/null +++ b/autotests/tabbox/test_tabbox_clientmodel.h @@ -0,0 +1,53 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..4a2774f --- /dev/null +++ b/autotests/tabbox/test_tabbox_config.cpp @@ -0,0 +1,85 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..e1bdf88 --- /dev/null +++ b/autotests/tabbox/test_tabbox_handler.cpp @@ -0,0 +1,63 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..ce5e307 --- /dev/null +++ b/autotests/test_builtin_effectloader.cpp @@ -0,0 +1,580 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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("DimScreen") << QStringLiteral("dimscreen") << 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("MinimizeAnimation") << QStringLiteral("minimizeanimation") << 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("Scale") << QStringLiteral("scale") << 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("dimscreen") + << QStringLiteral("fallapart") + << QStringLiteral("flipswitch") + << QStringLiteral("glide") + << QStringLiteral("highlightwindow") + << QStringLiteral("invert") + << QStringLiteral("kscreen") + << QStringLiteral("lookingglass") + << QStringLiteral("magiclamp") + << QStringLiteral("magnifier") + << QStringLiteral("minimizeanimation") + << QStringLiteral("mouseclick") + << QStringLiteral("mousemark") + << QStringLiteral("presentwindows") + << QStringLiteral("resize") + << QStringLiteral("scale") + << 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()); + qSort(result); + 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("DimScreen") << QStringLiteral("dimscreen") << 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("MinimizeAnimation") << QStringLiteral("minimizeanimation") << 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("Scale") << QStringLiteral("scale") << 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("DimScreen") << QStringLiteral("dimScreen") << 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("MinimizeAnimation") << QStringLiteral("minimizeanimation") << 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("Scale") << QStringLiteral("scale") << 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, SIGNAL(effectLoaded(KWin::Effect*,QString))); + // 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, SIGNAL(effectLoaded(KWin::Effect*,QString))); + // 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("minimizeanimationEnabled"), 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, SIGNAL(effectLoaded(KWin::Effect*,QString))); + // 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(); + } + qSort(loadedEffects); + 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..6c83ebe --- /dev/null +++ b/autotests/test_client_machine.cpp @@ -0,0 +1,157 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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, SIGNAL(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, SIGNAL(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..b1c7a3c --- /dev/null +++ b/autotests/test_gbm_surface.cpp @@ -0,0 +1,119 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..9c01e84 --- /dev/null +++ b/autotests/test_gestures.cpp @@ -0,0 +1,615 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..0575244 --- /dev/null +++ b/autotests/test_plugin_effectloader.cpp @@ -0,0 +1,419 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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("DimScreen") << QStringLiteral("dimscreen") << 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("MinimizeAnimation") << QStringLiteral("minimizeanimation") << 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("Scale") << QStringLiteral("scale") << 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("Fade") << QStringLiteral("kwin4_effect_fade") << false; + QTest::newRow("FadeDesktop") << QStringLiteral("kwin4_effect_fadedesktop") << false; + QTest::newRow("DialogParent") << QStringLiteral("kwin4_effect_dialogparent") << 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("Translucency") << QStringLiteral("kwin4_effect_translucency") << 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, SIGNAL(effectLoaded(KWin::Effect*,QString))); + // 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, SIGNAL(effectLoaded(KWin::Effect*,QString))); + // 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, SIGNAL(effectLoaded(KWin::Effect*,QString))); + // 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..a89d06b --- /dev/null +++ b/autotests/test_screen_edges.cpp @@ -0,0 +1,1087 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +// 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 "mock_client.h" +#include "mock_screens.h" +#include "mock_workspace.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; + +Cursor *Cursor::s_self = nullptr; +static QPoint s_cursorPos = QPoint(); +QPoint Cursor::pos() +{ + return s_cursorPos; +} + +void Cursor::setPos(const QPoint &pos) +{ + s_cursorPos = pos; +} + +void Cursor::setPos(int x, int y) +{ + setPos(QPoint(x, y)); +} + +void Cursor::startMousePolling() +{ +} +void Cursor::stopMousePolling() +{ +} + +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 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() +{ + using namespace KWin; + new MockWorkspace; + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + Screens::create(); + + 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(); + 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(), SIGNAL(changed())); + QVERIFY(changedSpy.isValid()); + // first is before it's updated + QVERIFY(changedSpy.wait()); + // second is after it's updated + 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. + Client client(workspace()); + workspace()->setMovingClient(&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()->setMovingClient(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(), SIGNAL(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, SIGNAL(gotCallback(KWin::ElectricBorder))); + 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) { + Cursor::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)); + }; + QVERIFY(isEntered(&event)); + // doesn't trigger as the edge was not triggered yet + QVERIFY(spy.isEmpty()); + QCOMPARE(Cursor::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(Cursor::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(Cursor::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(Cursor::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(Cursor::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(Cursor::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(Cursor::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(Cursor::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(Cursor::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, SIGNAL(gotCallback(KWin::ElectricBorder))); + QVERIFY(spy.isValid()); + s->reserve(ElectricLeft, &callback, "callback"); + + // check activating a different edge doesn't do anything + s->check(QPoint(50, 0), QDateTime::currentDateTime(), true); + QVERIFY(spy.isEmpty()); + + // try a direct activate without pushback + Cursor::setPos(0, 50); + s->check(QPoint(0, 50), QDateTime::currentDateTime(), true); + QCOMPARE(spy.count(), 1); + QEXPECT_FAIL("", "Argument says force no pushback, but it gets pushed back. Needs investigation", Continue); + QCOMPARE(Cursor::pos(), QPoint(0, 50)); + + // use a different edge, this time with pushback + s->reserve(KWin::ElectricRight, &callback, "callback"); + Cursor::setPos(99, 50); + s->check(QPoint(99, 50), QDateTime::currentDateTime()); + QCOMPARE(spy.count(), 1); + QCOMPARE(spy.last().first().value(), ElectricLeft); + QCOMPARE(Cursor::pos(), QPoint(98, 50)); + // and trigger it again + QTest::qWait(160); + Cursor::setPos(99, 50); + s->check(QPoint(99, 50), QDateTime::currentDateTime()); + QCOMPARE(spy.count(), 2); + QCOMPARE(spy.last().first().value(), ElectricRight); + QCOMPARE(Cursor::pos(), QPoint(98, 50)); +} + +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(); + + // TODO: add screens + + auto s = ScreenEdges::self(); + s->setConfig(config); + s->init(); + TestObject callback; + QSignalSpy spy(&callback, SIGNAL(gotCallback(KWin::ElectricBorder))); + QVERIFY(spy.isValid()); + QFETCH(ElectricBorder, border); + s->reserve(border, &callback, "callback"); + + QFETCH(QPoint, trigger); + Cursor::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(Cursor::pos(), "expected"); + + // do the same without the event, but the check method + Cursor::setPos(trigger); + s->check(trigger, QDateTime::currentDateTime()); + QVERIFY(spy.isEmpty()); + QTEST(Cursor::pos(), "expected"); +} + +void TestScreenEdges::testFullScreenBlocking() +{ + using namespace KWin; + MockWorkspace ws; + Client 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, SIGNAL(gotCallback(KWin::ElectricBorder))); + 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; + Cursor::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(Cursor::pos(), QPoint(1, 50)); + + client.setGeometry(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); + Cursor::setPos(0, 50); + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + // and no pushback + QCOMPARE(Cursor::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(Cursor::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.setGeometry(client.geometry().translated(10, 0)); + emit s->checkBlocking(); + spy.clear(); + Cursor::setPos(0, 50); + event.time = QDateTime::currentMSecsSinceEpoch(); + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + // and a pushback + QCOMPARE(Cursor::pos(), QPoint(1, 50)); + + // just to be sure, let's set geometry back + client.setGeometry(screens()->geometry()); + emit s->checkBlocking(); + Cursor::setPos(0, 50); + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + // and no pushback + QCOMPARE(Cursor::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(); + Cursor::setPos(99, 99); + QVERIFY(isEntered(&event)); + QVERIFY(spy.isEmpty()); + // and pushback + QCOMPARE(Cursor::pos(), QPoint(98, 98)); + QTest::qWait(160); + event.time = QDateTime::currentMSecsSinceEpoch(); + Cursor::setPos(99, 99); + QVERIFY(isEntered(&event)); + QVERIFY(!spy.isEmpty()); +} + +void TestScreenEdges::testClientEdge() +{ + using namespace KWin; + Client client(workspace()); + client.setGeometry(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.setGeometry(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.setGeometry(screens()->geometry()); + client.setHiddenInternal(true); + s->reserve(&client, KWin::ElectricLeft); + QCOMPARE(client.isHiddenInternal(), true); + + xcb_enter_notify_event_t event; + Cursor::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(Cursor::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); + Cursor::setPos(50, 0); + s->check(QPoint(50, 0), QDateTime::currentDateTime()); + QCOMPARE(client.isHiddenInternal(), false); + QCOMPARE(Cursor::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); + Cursor::setPos(50, 0); + s->check(QPoint(50, 0), QDateTime::currentDateTime()); + QCOMPARE(client.isHiddenInternal(), true); + QCOMPARE(Cursor::pos(), QPoint(50, 0)); + + // set to windows can cover + client.setGeometry(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; + Cursor::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(Cursor::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) { + Cursor::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)); + }; + QCOMPARE(isEntered(&event), false); + QVERIFY(approachingSpy.isEmpty()); + // let's also verify the check + s->check(QPoint(0, 50), QDateTime::currentDateTime(), 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..952a229 --- /dev/null +++ b/autotests/test_screen_paint_data.cpp @@ -0,0 +1,287 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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..172a1af --- /dev/null +++ b/autotests/test_screens.cpp @@ -0,0 +1,366 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "mock_workspace.h" +#include "../cursor.h" +#include "mock_screens.h" +#include "mock_client.h" +// frameworks +#include +// Qt +#include + +Q_LOGGING_CATEGORY(KWIN_CORE, "kwin_core") + +// Mock +namespace KWin +{ + +static QPoint s_cursorPos = QPoint(); +QPoint Cursor::pos() +{ + return s_cursorPos; +} + +} + +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::s_cursorPos = QPoint(); +} + +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(), SIGNAL(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(), SIGNAL(countChanged(int,int))); + 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(), SIGNAL(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(), SIGNAL(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(), SIGNAL(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(), SIGNAL(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(), SIGNAL(currentChanged())); + QVERIFY(currentChangedSpy.isValid()); + + // create a mock client + Client *client = new Client(&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(), SIGNAL(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::s_cursorPos = 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(), SIGNAL(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..f1a729d --- /dev/null +++ b/autotests/test_scripted_effectloader.cpp @@ -0,0 +1,450 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 *) +{ +} +} + +static QPoint s_cursorPos = QPoint(); +QPoint Cursor::pos() +{ + return s_cursorPos; +} + + +} + +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() +{ + auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); + QCoreApplication::instance()->setProperty("config", QVariant::fromValue(config)); +} + +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("DimScreen") << QStringLiteral("dimscreen") << 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("MinimizeAnimation") << QStringLiteral("minimizeanimation") << 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("Scale") << QStringLiteral("scale") << 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("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("FrozenApp") << QStringLiteral("kwin4_effect_frozenapp") << true; + QTest::newRow("DialogParent") << QStringLiteral("kwin4_effect_dialogparent") << 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("Translucency") << QStringLiteral("kwin4_effect_translucency") << 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_fade") + << QStringLiteral("kwin4_effect_fadedesktop") + << QStringLiteral("kwin4_effect_frozenapp") + << QStringLiteral("kwin4_effect_login") + << QStringLiteral("kwin4_effect_logout") + << QStringLiteral("kwin4_effect_maximize") + << QStringLiteral("kwin4_effect_translucency"); + + 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("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("FrozenApp") << QStringLiteral("kwin4_effect_frozenapp") << true; + QTest::newRow("DialogParent") << QStringLiteral("kwin4_effect_dialogparent") << 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("Translucency") << QStringLiteral("kwin4_effect_translucency") << 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, SIGNAL(effectLoaded(KWin::Effect*,QString))); + // 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, SIGNAL(effectLoaded(KWin::Effect*,QString))); + // 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("fadeEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("fadedesktopEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("frozenappEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("loginEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("logoutEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("maximizeEnabled"), false); + plugins.writeEntry(kwin4 + QStringLiteral("minimizeanimationEnabled"), 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.sync(); + + loader.setConfig(config); + + qRegisterMetaType(); + QSignalSpy spy(&loader, SIGNAL(effectLoaded(KWin::Effect*,QString))); + // 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(); + } + qSort(loadedEffects); + 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..feb6b85 --- /dev/null +++ b/autotests/test_virtual_desktops.cpp @@ -0,0 +1,656 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 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, SIGNAL(countChanged(uint,uint))); + QSignalSpy desktopsRemoved(vds, SIGNAL(desktopsRemoved(uint))); + + 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).type(), QVariant::UInt); + QCOMPARE(arguments.at(0).toUInt(), s_countInitValue); + } +} + +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, SIGNAL(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, SIGNAL(currentChanged(uint,uint))); + + 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, SIGNAL(currentChanged(uint,uint))); + + 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, SIGNAL(layoutChanged(int,int))); + // 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::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 screen number should reset to one desktop as config value is missing + screen_number = 2; + vds->load(); + QCOMPARE(vds->count(), (uint)1); + // creating the respective group should properly load + config->group("Desktops-screen-2").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); + + // change screen number + screen_number = 3; + QCOMPARE(config->hasGroup("Desktops-screen-3"), false); + vds->setCount(3); + vds->save(); + QCOMPARE(config->hasGroup("Desktops-screen-3"), true); + // old one should be unchanged + desktops = config->group("Desktops"); + QCOMPARE(desktops.readEntry("Number", 1), 4); + desktops = config->group("Desktops-screen-3"); + QCOMPARE(desktops.readEntry("Number", 1), 3); + 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..ba83c87 --- /dev/null +++ b/autotests/test_virtualkeyboard_dbus.cpp @@ -0,0 +1,142 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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() = 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..8b8c9f7 --- /dev/null +++ b/autotests/test_window_paint_data.cpp @@ -0,0 +1,333 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ + +#include + +#include +#include +#include + +#include + +using namespace KWin; + +class MockEffectWindowHelper : public QObject +{ + Q_OBJECT + Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity) +public: + MockEffectWindowHelper(QObject *parent = nullptr); + double opacity() const { + return m_opacity; + } + void setOpacity(qreal opacity) { + m_opacity = opacity; + } +private: + qreal m_opacity; +}; + +MockEffectWindowHelper::MockEffectWindowHelper(QObject *parent) + : QObject(parent) + , m_opacity(1.0) +{ +} + +class MockEffectWindow : public EffectWindow +{ + Q_OBJECT +public: + MockEffectWindow(QObject *parent = nullptr); + virtual WindowQuadList buildQuads(bool force = false) const; + virtual QVariant data(int role) const; + virtual QRect decorationInnerRect() const; + virtual void deleteProperty(long int atom) const; + virtual void disablePainting(int reason); + virtual void enablePainting(int reason); + virtual EffectWindow *findModal(); + virtual const EffectWindowGroup *group() const; + virtual bool isPaintingEnabled(); + virtual EffectWindowList mainWindows() const; + virtual QByteArray readProperty(long int atom, long int type, int format) const; + virtual void refWindow(); + virtual void unrefWindow(); + virtual QRegion shape() const; + virtual void setData(int role, const QVariant &data); + virtual void referencePreviousWindowPixmap() {} + virtual void unreferencePreviousWindowPixmap() {} +}; + +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) +} + +EffectWindow *MockEffectWindow::findModal() +{ + 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) +} + +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() +{ + MockEffectWindowHelper helper; + helper.setOpacity(0.5); + MockEffectWindow w(&helper); + 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() +{ + MockEffectWindowHelper helper; + MockEffectWindow w(&helper); + 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() +{ + MockEffectWindowHelper helper; + MockEffectWindow w(&helper); + 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() +{ + MockEffectWindowHelper helper; + MockEffectWindow w(&helper); + 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() +{ + MockEffectWindowHelper helper; + MockEffectWindow w(&helper); + 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() +{ + MockEffectWindowHelper helper; + MockEffectWindow w(&helper); + 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() +{ + MockEffectWindowHelper helper; + MockEffectWindow w(&helper); + 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..9bfbe19 --- /dev/null +++ b/autotests/test_x11_timestamp_update.cpp @@ -0,0 +1,126 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2017 Martin Gräßlin + +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, see . +*********************************************************************/ + +#include +#include + +#include + +#include "main.h" +#include "utils.h" + +namespace KWin +{ + +class X11TestApplication : public Application +{ + Q_OBJECT +public: + X11TestApplication(int &argc, char **argv); + virtual ~X11TestApplication(); + +protected: + void performStartup() override; + +}; + +X11TestApplication::X11TestApplication(int &argc, char **argv) + : Application(OperationModeX11, argc, argv) +{ + setX11Connection(QX11Info::connection()); + setX11RootWindow(QX11Info::appRootWindow()); + initPlatform(KPluginMetaData(QStringLiteral("KWinX11Platform.so"))); +} + +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..61ca2c4 --- /dev/null +++ b/autotests/test_xcb_size_hints.cpp @@ -0,0 +1,377 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..236f123 --- /dev/null +++ b/autotests/test_xcb_window.cpp @@ -0,0 +1,213 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..32719b5 --- /dev/null +++ b/autotests/test_xcb_wrapper.cpp @@ -0,0 +1,531 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..7714f09 --- /dev/null +++ b/autotests/test_xkb.cpp @@ -0,0 +1,513 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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/test_xrandr_screens.cpp b/autotests/test_xrandr_screens.cpp new file mode 100644 index 0000000..e0f6b14 --- /dev/null +++ b/autotests/test_xrandr_screens.cpp @@ -0,0 +1,287 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "../plugins/platforms/x11/standalone/screens_xrandr.h" +#include "../cursor.h" +#include "../xcbutils.h" +#include "mock_workspace.h" +// Qt +#include +// system +#include + +Q_LOGGING_CATEGORY(KWIN_CORE, "kwin_core") + +// mocking +namespace KWin +{ + +QPoint Cursor::pos() +{ + return QPoint(0, 0); +} +} // namespace KWin + +static xcb_window_t s_rootWindow = XCB_WINDOW_NONE; +static xcb_connection_t *s_connection = nullptr; + +using namespace KWin; +using namespace KWin::Xcb; + +class TestXRandRScreens : public QObject +{ + Q_OBJECT +private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + void testStartup(); + void testChange(); + void testMultipleChanges(); +private: + QScopedPointer m_xserver; +}; + +void TestXRandRScreens::initTestCase() +{ + // TODO: turn into init instead of initTestCase + // needs to be initTestCase as KWin::connection caches the first created xcb_connection_t + // thus changing X server for each test run would create problems + qsrand(QDateTime::currentMSecsSinceEpoch()); + // first reset just to be sure + s_connection = nullptr; + s_rootWindow = XCB_WINDOW_NONE; + // start X Server + m_xserver.reset(new QProcess); + // use pipe to pass fd to Xephyr to get back the display id + int pipeFds[2]; + QVERIFY(pipe(pipeFds) == 0); + // using Xephyr as Xvfb doesn't support render extension + m_xserver->start(QStringLiteral("Xephyr"), QStringList({ QStringLiteral("-displayfd"), QString::number(pipeFds[1]) })); + QVERIFY(m_xserver->waitForStarted()); + QCOMPARE(m_xserver->state(), QProcess::Running); + + // reads from pipe, closes write side + close(pipeFds[1]); + + QFile readPipe; + QVERIFY(readPipe.open(pipeFds[0], QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)); + QByteArray displayNumber = readPipe.readLine(); + readPipe.close(); + + displayNumber.prepend(QByteArray(":")); + displayNumber.remove(displayNumber.size() -1, 1); + + // create X connection + int screen = 0; + s_connection = xcb_connect(displayNumber.constData(), &screen); + QVERIFY(s_connection); + + // set root window + xcb_screen_iterator_t iter = xcb_setup_roots_iterator(xcb_get_setup(s_connection)); + for (xcb_screen_iterator_t it = xcb_setup_roots_iterator(xcb_get_setup(s_connection)); + it.rem; + --screen, xcb_screen_next(&it)) { + if (screen == 0) { + s_rootWindow = iter.data->root; + break; + } + } + QVERIFY(s_rootWindow != XCB_WINDOW_NONE); + qApp->setProperty("x11RootWindow", QVariant::fromValue(s_rootWindow)); + qApp->setProperty("x11Connection", QVariant::fromValue(s_connection)); + + // get the extensions + if (!Extensions::self()->isRandrAvailable()) { + QSKIP("XRandR extension required"); + } + for (const auto &extension : Extensions::self()->extensions()) { + if (extension.name == QByteArrayLiteral("RANDR")) { + if (extension.version < 1 * 0x10 + 4) { + QSKIP("At least XRandR 1.4 required"); + } + } + } +} + +void TestXRandRScreens::cleanupTestCase() +{ + Extensions::destroy(); + // close connection + xcb_disconnect(s_connection); + s_connection = nullptr; + s_rootWindow = XCB_WINDOW_NONE; + // kill X + m_xserver->terminate(); + m_xserver->waitForFinished(); +} + +void TestXRandRScreens::testStartup() +{ + KWin::MockWorkspace ws; + QScopedPointer screens(new XRandRScreens(this)); + QVERIFY(!screens->eventTypes().isEmpty()); + QCOMPARE(screens->eventTypes().first(), Xcb::Extensions::self()->randrNotifyEvent()); + QCOMPARE(screens->extension(), 0); + QCOMPARE(screens->genericEventTypes(), QVector{0}); + screens->init(); + QRect xephyrDefault = QRect(0, 0, 640, 480); + QCOMPARE(screens->count(), 1); + QCOMPARE(screens->geometry(0), xephyrDefault); + QCOMPARE(screens->geometry(1), QRect()); + QCOMPARE(screens->geometry(-1), QRect()); + QCOMPARE(static_cast(screens.data())->geometry(), xephyrDefault); + QCOMPARE(screens->size(0), xephyrDefault.size()); + QCOMPARE(screens->size(1), QSize()); + QCOMPARE(screens->size(-1), QSize()); + QCOMPARE(static_cast(screens.data())->size(), xephyrDefault.size()); + // unfortunately we only have one output, so let's try at least to test somewhat + QCOMPARE(screens->number(QPoint(0, 0)), 0); + QCOMPARE(screens->number(QPoint(639, 479)), 0); + QCOMPARE(screens->number(QPoint(1280, 1024)), 0); + + // let's change the mode + RandR::CurrentResources resources(s_rootWindow); + auto *crtcs = resources.crtcs(); + auto *modes = xcb_randr_get_screen_resources_current_modes(resources.data()); + auto *outputs = xcb_randr_get_screen_resources_current_outputs(resources.data()); + RandR::SetCrtcConfig setter(crtcs[0], resources->timestamp, resources->config_timestamp, 0, 0, modes[0].id, XCB_RANDR_ROTATION_ROTATE_0, 1, outputs); + QVERIFY(!setter.isNull()); + + // now let's recreate the XRandRScreens + screens.reset(new XRandRScreens(this)); + screens->init(); + QRect geo = QRect(0, 0, modes[0].width, modes[0].height); + QCOMPARE(screens->count(), 1); + QCOMPARE(screens->geometry(0), geo); + QCOMPARE(static_cast(screens.data())->geometry(), geo); + QCOMPARE(screens->size(0), geo.size()); + QCOMPARE(static_cast(screens.data())->size(), geo.size()); +} + +void TestXRandRScreens::testChange() +{ + KWin::MockWorkspace ws; + QScopedPointer screens(new XRandRScreens(this)); + screens->init(); + + // create some signal spys + QSignalSpy changedSpy(screens.data(), SIGNAL(changed())); + QVERIFY(changedSpy.isValid()); + QVERIFY(changedSpy.isEmpty()); + QVERIFY(changedSpy.wait()); + changedSpy.clear(); + QSignalSpy geometrySpy(screens.data(), SIGNAL(geometryChanged())); + QVERIFY(geometrySpy.isValid()); + QVERIFY(geometrySpy.isEmpty()); + QSignalSpy sizeSpy(screens.data(), SIGNAL(sizeChanged())); + QVERIFY(sizeSpy.isValid()); + QVERIFY(sizeSpy.isEmpty()); + + // clear the event loop + while (xcb_generic_event_t *e = xcb_poll_for_event(s_connection)) { + free(e); + } + + // let's change + RandR::CurrentResources resources(s_rootWindow); + auto *crtcs = resources.crtcs(); + auto *modes = xcb_randr_get_screen_resources_current_modes(resources.data()); + auto *outputs = xcb_randr_get_screen_resources_current_outputs(resources.data()); + RandR::SetCrtcConfig setter(crtcs[0], resources->timestamp, resources->config_timestamp, 0, 0, modes[1].id, XCB_RANDR_ROTATION_ROTATE_0, 1, outputs); + xcb_flush(s_connection); + QVERIFY(!setter.isNull()); + QVERIFY(setter->status == XCB_RANDR_SET_CONFIG_SUCCESS); + + xcb_generic_event_t *e = xcb_wait_for_event(s_connection); + screens->event(e); + free(e); + + QVERIFY(changedSpy.wait()); + QCOMPARE(changedSpy.size(), 1); + QCOMPARE(sizeSpy.size(), 1); + QCOMPARE(geometrySpy.size(), 1); + QRect geo = QRect(0, 0, modes[1].width, modes[1].height); + QCOMPARE(screens->count(), 1); + QCOMPARE(screens->geometry(0), geo); + QCOMPARE(static_cast(screens.data())->geometry(), geo); + QCOMPARE(screens->size(0), geo.size()); + QCOMPARE(static_cast(screens.data())->size(), geo.size()); +} + +void TestXRandRScreens::testMultipleChanges() +{ + KWin::MockWorkspace ws; + // multiple changes should only hit one changed signal + QScopedPointer screens(new XRandRScreens(this)); + screens->init(); + + // create some signal spys + QSignalSpy changedSpy(screens.data(), SIGNAL(changed())); + QVERIFY(changedSpy.isValid()); + QVERIFY(changedSpy.isEmpty()); + QVERIFY(changedSpy.wait()); + changedSpy.clear(); + QSignalSpy geometrySpy(screens.data(), SIGNAL(geometryChanged())); + QVERIFY(geometrySpy.isValid()); + QVERIFY(geometrySpy.isEmpty()); + QSignalSpy sizeSpy(screens.data(), SIGNAL(sizeChanged())); + QVERIFY(sizeSpy.isValid()); + QVERIFY(sizeSpy.isEmpty()); + + // clear the event loop + while (xcb_generic_event_t *e = xcb_poll_for_event(s_connection)) { + free(e); + } + + // first change + RandR::CurrentResources resources(s_rootWindow); + auto *crtcs = resources.crtcs(); + auto *modes = xcb_randr_get_screen_resources_current_modes(resources.data()); + auto *outputs = xcb_randr_get_screen_resources_current_outputs(resources.data()); + RandR::SetCrtcConfig setter(crtcs[0], resources->timestamp, resources->config_timestamp, 0, 0, modes[0].id, XCB_RANDR_ROTATION_ROTATE_0, 1, outputs); + QVERIFY(!setter.isNull()); + QVERIFY(setter->status == XCB_RANDR_SET_CONFIG_SUCCESS); + // second change + RandR::SetCrtcConfig setter2(crtcs[0], setter->timestamp, resources->config_timestamp, 0, 0, modes[1].id, XCB_RANDR_ROTATION_ROTATE_0, 1, outputs); + QVERIFY(!setter2.isNull()); + QVERIFY(setter2->status == XCB_RANDR_SET_CONFIG_SUCCESS); + + auto passEvent = [&screens]() { + xcb_generic_event_t *e = xcb_wait_for_event(s_connection); + screens->event(e); + free(e); + }; + passEvent(); + passEvent(); + + QVERIFY(changedSpy.wait()); + QCOMPARE(changedSpy.size(), 1); + // previous state was modes[1] so the size didn't change + QVERIFY(sizeSpy.isEmpty()); + QVERIFY(geometrySpy.isEmpty()); + QRect geo = QRect(0, 0, modes[1].width, modes[1].height); + QCOMPARE(screens->count(), 1); + QCOMPARE(screens->geometry(0), geo); + QCOMPARE(static_cast(screens.data())->geometry(), geo); + QCOMPARE(screens->size(0), geo.size()); + QCOMPARE(static_cast(screens.data())->size(), geo.size()); +} + +QTEST_GUILESS_MAIN(TestXRandRScreens) +#include "test_xrandr_screens.moc" diff --git a/autotests/testutils.h b/autotests/testutils.h new file mode 100644 index 0000000..103a81c --- /dev/null +++ b/autotests/testutils.h @@ -0,0 +1,63 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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/client.cpp b/client.cpp new file mode 100644 index 0000000..9a8f2c4 --- /dev/null +++ b/client.cpp @@ -0,0 +1,2100 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ +// own +#include "client.h" +// kwin +#ifdef KWIN_BUILD_ACTIVITIES +#include "activities.h" +#endif +#include "atoms.h" +#include "client_machine.h" +#include "composite.h" +#include "cursor.h" +#include "deleted.h" +#include "focuschain.h" +#include "group.h" +#include "shadow.h" +#include "workspace.h" +#include "screenedge.h" +#include "decorations/decorationbridge.h" +#include "decorations/decoratedclient.h" +#include +#include +// KDE +#include +#include +#include +// Qt +#include +#include +#include +#include +#include +// XLib +#include +#include +#include +// system +#include +#include + +// Put all externs before the namespace statement to allow the linker +// to resolve them properly + +namespace KWin +{ + +const long ClientWinMask = XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE | + XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | + XCB_EVENT_MASK_KEYMAP_STATE | + XCB_EVENT_MASK_BUTTON_MOTION | + XCB_EVENT_MASK_POINTER_MOTION | // need this, too! + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW | + XCB_EVENT_MASK_FOCUS_CHANGE | + XCB_EVENT_MASK_EXPOSURE | + XCB_EVENT_MASK_STRUCTURE_NOTIFY | + XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT; + +// Creating a client: +// - only by calling Workspace::createClient() +// - it creates a new client and calls manage() for it +// +// Destroying a client: +// - destroyClient() - only when the window itself has been destroyed +// - releaseWindow() - the window is kept, only the client itself is destroyed + +/** + * \class Client client.h + * \brief The Client class encapsulates a window decoration frame. + */ + +/** + * This ctor is "dumb" - it only initializes data. All the real initialization + * is done in manage(). + */ +Client::Client() + : AbstractClient() + , m_client() + , m_wrapper() + , m_frame() + , m_activityUpdatesBlocked(false) + , m_blockedActivityUpdatesRequireTransients(false) + , m_moveResizeGrabWindow() + , move_resize_has_keyboard_grab(false) + , m_managed(false) + , m_transientForId(XCB_WINDOW_NONE) + , m_originalTransientForId(XCB_WINDOW_NONE) + , shade_below(NULL) + , m_motif(atoms->motif_wm_hints) + , blocks_compositing(false) + , shadeHoverTimer(NULL) + , m_colormap(XCB_COLORMAP_NONE) + , in_group(NULL) + , ping_timer(NULL) + , m_killHelperPID(0) + , m_pingTimestamp(XCB_TIME_CURRENT_TIME) + , m_userTime(XCB_TIME_CURRENT_TIME) // Not known yet + , allowed_actions(0) + , shade_geometry_change(false) + , sm_stacking_order(-1) + , activitiesDefined(false) + , sessionActivityOverride(false) + , needsXWindowMove(false) + , m_decoInputExtent() + , m_focusOutTimer(nullptr) + , m_clientSideDecorated(false) +{ + // TODO: Do all as initialization + syncRequest.counter = syncRequest.alarm = XCB_NONE; + syncRequest.timeout = syncRequest.failsafeTimeout = NULL; + syncRequest.lastTimestamp = xTime(); + syncRequest.isPending = false; + + // Set the initial mapping state + mapping_state = Withdrawn; + + info = NULL; + + shade_mode = ShadeNone; + deleting = false; + fullscreen_mode = FullScreenNone; + hidden = false; + noborder = false; + app_noborder = false; + ignore_focus_stealing = false; + check_active_modal = false; + + max_mode = MaximizeRestore; + + //Client to workspace connections require that each + //client constructed be connected to the workspace wrapper + + geom = QRect(0, 0, 100, 100); // So that decorations don't start with size being (0,0) + client_size = QSize(100, 100); + ready_for_painting = false; // wait for first damage or sync reply + + connect(clientMachine(), &ClientMachine::localhostChanged, this, &Client::updateCaption); + connect(options, &Options::condensedTitleChanged, this, &Client::updateCaption); + + connect(this, &Client::moveResizeCursorChanged, this, [this] (CursorShape cursor) { + xcb_cursor_t nativeCursor = Cursor::x11Cursor(cursor); + m_frame.defineCursor(nativeCursor); + if (m_decoInputExtent.isValid()) + m_decoInputExtent.defineCursor(nativeCursor); + if (isMoveResize()) { + // changing window attributes doesn't change cursor if there's pointer grab active + xcb_change_active_pointer_grab(connection(), nativeCursor, xTime(), + 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); + } + }); + + connect(this, &Client::tabGroupChanged, this, + [this] { + auto group = tabGroup(); + if (group) { + unsigned long data[] = {qHash(group)}; //->id(); + m_client.changeProperty(atoms->kde_net_wm_tab_group, XCB_ATOM_CARDINAL, 32, 1, data); + } + else + m_client.deleteProperty(atoms->kde_net_wm_tab_group); + }); + + // SELI TODO: Initialize xsizehints?? +} + +/** + * "Dumb" destructor. + */ +Client::~Client() +{ + if (m_killHelperPID && !::kill(m_killHelperPID, 0)) { // means the process is alive + ::kill(m_killHelperPID, SIGTERM); + m_killHelperPID = 0; + } + //SWrapper::Client::clientRelease(this); + if (syncRequest.alarm != XCB_NONE) + xcb_sync_destroy_alarm(connection(), syncRequest.alarm); + assert(!isMoveResize()); + assert(m_client == XCB_WINDOW_NONE); + assert(m_wrapper == XCB_WINDOW_NONE); + //assert( frameId() == None ); + assert(!check_active_modal); + for (auto it = m_connections.constBegin(); it != m_connections.constEnd(); ++it) { + disconnect(*it); + } +} + +// Use destroyClient() or releaseWindow(), Client instances cannot be deleted directly +void Client::deleteClient(Client* c) +{ + delete c; +} + +/** + * Releases the window. The client has done its job and the window is still existing. + */ +void Client::releaseWindow(bool on_shutdown) +{ + assert(!deleting); + deleting = true; + destroyWindowManagementInterface(); + Deleted* del = NULL; + if (!on_shutdown) { + del = Deleted::create(this); + } + if (isMoveResize()) + emit clientFinishUserMovedResized(this); + emit windowClosed(this, del); + finishCompositing(); + RuleBook::self()->discardUsed(this, true); // Remove ForceTemporarily rules + StackingUpdatesBlocker blocker(workspace()); + if (isMoveResize()) + leaveMoveResize(); + finishWindowRules(); + blockGeometryUpdates(); + if (isOnCurrentDesktop() && isShown(true)) + addWorkspaceRepaint(visibleRect()); + // Grab X during the release to make removing of properties, setting to withdrawn state + // and repareting to root an atomic operation (http://lists.kde.org/?l=kde-devel&m=116448102901184&w=2) + grabXServer(); + exportMappingState(WithdrawnState); + setModal(false); // Otherwise its mainwindow wouldn't get focus + hidden = true; // So that it's not considered visible anymore (can't use hideClient(), it would set flags) + if (!on_shutdown) + workspace()->clientHidden(this); + m_frame.unmap(); // Destroying decoration would cause ugly visual effect + destroyDecoration(); + cleanGrouping(); + if (!on_shutdown) { + workspace()->removeClient(this); + // Only when the window is being unmapped, not when closing down KWin (NETWM sections 5.5,5.7) + info->setDesktop(0); + info->setState(0, info->state()); // Reset all state flags + } else + untab(); + xcb_connection_t *c = connection(); + m_client.deleteProperty(atoms->kde_net_wm_user_creation_time); + m_client.deleteProperty(atoms->net_frame_extents); + m_client.deleteProperty(atoms->kde_net_wm_frame_strut); + m_client.reparent(rootWindow(), x(), y()); + xcb_change_save_set(c, XCB_SET_MODE_DELETE, m_client); + m_client.selectInput(XCB_EVENT_MASK_NO_EVENT); + if (on_shutdown) + // Map the window, so it can be found after another WM is started + m_client.map(); + // TODO: Preserve minimized, shaded etc. state? + else // Make sure it's not mapped if the app unmapped it (#65279). The app + // may do map+unmap before we initially map the window by calling rawShow() from manage(). + m_client.unmap(); + m_client.reset(); + m_wrapper.reset(); + m_frame.reset(); + //frame = None; + unblockGeometryUpdates(); // Don't use GeometryUpdatesBlocker, it would now set the geometry + if (!on_shutdown) { + disownDataPassedToDeleted(); + del->unrefWindow(); + } + deleteClient(this); + ungrabXServer(); +} + +/** + * Like releaseWindow(), but this one is called when the window has been already destroyed + * (E.g. The application closed it) + */ +void Client::destroyClient() +{ + assert(!deleting); + deleting = true; + destroyWindowManagementInterface(); + Deleted* del = Deleted::create(this); + if (isMoveResize()) + emit clientFinishUserMovedResized(this); + emit windowClosed(this, del); + finishCompositing(ReleaseReason::Destroyed); + RuleBook::self()->discardUsed(this, true); // Remove ForceTemporarily rules + StackingUpdatesBlocker blocker(workspace()); + if (isMoveResize()) + leaveMoveResize(); + finishWindowRules(); + blockGeometryUpdates(); + if (isOnCurrentDesktop() && isShown(true)) + addWorkspaceRepaint(visibleRect()); + setModal(false); + hidden = true; // So that it's not considered visible anymore + workspace()->clientHidden(this); + destroyDecoration(); + cleanGrouping(); + workspace()->removeClient(this); + m_client.reset(); // invalidate + m_wrapper.reset(); + m_frame.reset(); + //frame = None; + unblockGeometryUpdates(); // Don't use GeometryUpdatesBlocker, it would now set the geometry + disownDataPassedToDeleted(); + del->unrefWindow(); + deleteClient(this); +} + +void Client::updateInputWindow() +{ + if (!Xcb::Extensions::self()->isShapeInputAvailable()) + return; + + QRegion region; + + if (!noBorder() && isDecorated()) { + const QMargins &r = decoration()->resizeOnlyBorders(); + const int left = r.left(); + const int top = r.top(); + const int right = r.right(); + const int bottom = r.bottom(); + if (left != 0 || top != 0 || right != 0 || bottom != 0) { + region = QRegion(-left, + -top, + decoration()->size().width() + left + right, + decoration()->size().height() + top + bottom); + region = region.subtracted(decoration()->rect()); + } + } + + if (region.isEmpty()) { + m_decoInputExtent.reset(); + return; + } + + QRect bounds = region.boundingRect(); + input_offset = bounds.topLeft(); + + // Move the bounding rect to screen coordinates + bounds.translate(geometry().topLeft()); + + // Move the region to input window coordinates + region.translate(-input_offset); + + if (!m_decoInputExtent.isValid()) { + 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_BUTTON_PRESS | + XCB_EVENT_MASK_BUTTON_RELEASE | + XCB_EVENT_MASK_POINTER_MOTION + }; + m_decoInputExtent.create(bounds, XCB_WINDOW_CLASS_INPUT_ONLY, mask, values); + if (mapping_state == Mapped) + m_decoInputExtent.map(); + } else { + m_decoInputExtent.setGeometry(bounds); + } + + const QVector rects = Xcb::regionToRects(region); + xcb_shape_rectangles(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_CLIP_ORDERING_UNSORTED, + m_decoInputExtent, 0, 0, rects.count(), rects.constData()); +} + +void Client::updateDecoration(bool check_workspace_pos, bool force) +{ + if (!force && + ((!isDecorated() && noBorder()) || (isDecorated() && !noBorder()))) + return; + QRect oldgeom = geometry(); + QRect oldClientGeom = oldgeom.adjusted(borderLeft(), borderTop(), -borderRight(), -borderBottom()); + blockGeometryUpdates(true); + if (force) + destroyDecoration(); + if (!noBorder()) { + createDecoration(oldgeom); + } else + destroyDecoration(); + getShadow(); + if (check_workspace_pos) + checkWorkspacePosition(oldgeom, -2, oldClientGeom); + updateInputWindow(); + blockGeometryUpdates(false); + updateFrameExtents(); +} + +void Client::createDecoration(const QRect& oldgeom) +{ + KDecoration2::Decoration *decoration = Decoration::DecorationBridge::self()->createDecoration(this); + if (decoration) { + QMetaObject::invokeMethod(decoration, "update", Qt::QueuedConnection); + connect(decoration, &KDecoration2::Decoration::shadowChanged, this, &Toplevel::getShadow); + connect(decoration, &KDecoration2::Decoration::resizeOnlyBordersChanged, this, &Client::updateInputWindow); + connect(decoration, &KDecoration2::Decoration::bordersChanged, this, + [this]() { + updateFrameExtents(); + GeometryUpdatesBlocker blocker(this); + // TODO: this is obviously idempotent + // calculateGravitation(true) would have to operate on the old border sizes +// move(calculateGravitation(true)); +// move(calculateGravitation(false)); + QRect oldgeom = geometry(); + plainResize(sizeForClientSize(clientSize()), ForceGeometrySet); + if (!isShade()) + checkWorkspacePosition(oldgeom); + emit geometryShapeChanged(this, oldgeom); + } + ); + connect(decoratedClient()->decoratedClient(), &KDecoration2::DecoratedClient::widthChanged, this, &Client::updateInputWindow); + connect(decoratedClient()->decoratedClient(), &KDecoration2::DecoratedClient::heightChanged, this, &Client::updateInputWindow); + } + setDecoration(decoration); + + move(calculateGravitation(false)); + plainResize(sizeForClientSize(clientSize()), ForceGeometrySet); + if (Compositor::compositing()) { + discardWindowPixmap(); + } + emit geometryShapeChanged(this, oldgeom); +} + +void Client::destroyDecoration() +{ + QRect oldgeom = geometry(); + if (isDecorated()) { + QPoint grav = calculateGravitation(true); + AbstractClient::destroyDecoration(); + plainResize(sizeForClientSize(clientSize()), ForceGeometrySet); + move(grav); + if (compositing()) + discardWindowPixmap(); + if (!deleting) { + emit geometryShapeChanged(this, oldgeom); + } + } + m_decoInputExtent.reset(); +} + +void Client::layoutDecorationRects(QRect &left, QRect &top, QRect &right, QRect &bottom) const +{ + if (!isDecorated()) { + return; + } + QRect r = decoration()->rect(); + + NETStrut strut = info->frameOverlap(); + + // Ignore the overlap strut when compositing is disabled + if (!compositing()) + strut.left = strut.top = strut.right = strut.bottom = 0; + else if (strut.left == -1 && strut.top == -1 && strut.right == -1 && strut.bottom == -1) { + top = QRect(r.x(), r.y(), r.width(), r.height() / 3); + left = QRect(r.x(), r.y() + top.height(), width() / 2, r.height() / 3); + right = QRect(r.x() + left.width(), r.y() + top.height(), r.width() - left.width(), left.height()); + bottom = QRect(r.x(), r.y() + top.height() + left.height(), r.width(), r.height() - left.height() - top.height()); + return; + } + + top = QRect(r.x(), r.y(), r.width(), borderTop() + strut.top); + bottom = QRect(r.x(), r.y() + r.height() - borderBottom() - strut.bottom, + r.width(), borderBottom() + strut.bottom); + left = QRect(r.x(), r.y() + top.height(), + borderLeft() + strut.left, r.height() - top.height() - bottom.height()); + right = QRect(r.x() + r.width() - borderRight() - strut.right, r.y() + top.height(), + borderRight() + strut.right, r.height() - top.height() - bottom.height()); +} + +QRect Client::transparentRect() const +{ + if (isShade()) + return QRect(); + + NETStrut strut = info->frameOverlap(); + // Ignore the strut when compositing is disabled or the decoration doesn't support it + if (!compositing()) + strut.left = strut.top = strut.right = strut.bottom = 0; + else if (strut.left == -1 && strut.top == -1 && strut.right == -1 && strut.bottom == -1) + return QRect(); + + const QRect r = QRect(clientPos(), clientSize()) + .adjusted(strut.left, strut.top, -strut.right, -strut.bottom); + if (r.isValid()) + return r; + + return QRect(); +} + +void Client::detectNoBorder() +{ + if (shape()) { + noborder = true; + app_noborder = true; + return; + } + switch(windowType()) { + case NET::Desktop : + case NET::Dock : + case NET::TopMenu : + case NET::Splash : + case NET::Notification : + case NET::OnScreenDisplay : + noborder = true; + app_noborder = true; + break; + case NET::Unknown : + case NET::Normal : + case NET::Toolbar : + case NET::Menu : + case NET::Dialog : + case NET::Utility : + noborder = false; + break; + default: + abort(); + } + // NET::Override is some strange beast without clear definition, usually + // just meaning "noborder", so let's treat it only as such flag, and ignore it as + // a window type otherwise (SUPPORTED_WINDOW_TYPES_MASK doesn't include it) + if (info->windowType(NET::OverrideMask) == NET::Override) { + noborder = true; + app_noborder = true; + } +} + +void Client::updateFrameExtents() +{ + NETStrut strut; + strut.left = borderLeft(); + strut.right = borderRight(); + strut.top = borderTop(); + strut.bottom = borderBottom(); + info->setFrameExtents(strut); +} + +Xcb::Property Client::fetchGtkFrameExtents() const +{ + return Xcb::Property(false, m_client, atoms->gtk_frame_extents, XCB_ATOM_CARDINAL, 0, 4); +} + +void Client::readGtkFrameExtents(Xcb::Property &prop) +{ + m_clientSideDecorated = !prop.isNull() && prop->type != 0; + emit clientSideDecoratedChanged(); +} + +void Client::detectGtkFrameExtents() +{ + Xcb::Property prop = fetchGtkFrameExtents(); + readGtkFrameExtents(prop); +} + +/** + * Resizes the decoration, and makes sure the decoration widget gets resize event + * even if the size hasn't changed. This is needed to make sure the decoration + * re-layouts (e.g. when maximization state changes, + * the decoration may alter some borders, but the actual size + * of the decoration stays the same). + */ +void Client::resizeDecoration() +{ + triggerDecorationRepaint(); + updateInputWindow(); +} + +bool Client::noBorder() const +{ + return noborder || isFullScreen(); +} + +bool Client::userCanSetNoBorder() const +{ + return !isFullScreen() && !isShade() && !tabGroup(); +} + +void Client::setNoBorder(bool set) +{ + if (!userCanSetNoBorder()) + return; + set = rules()->checkNoBorder(set); + if (noborder == set) + return; + noborder = set; + updateDecoration(true, false); + updateWindowRules(Rules::NoBorder); +} + +void Client::checkNoBorder() +{ + setNoBorder(app_noborder); +} + +bool Client::wantsShadowToBeRendered() const +{ + return !isFullScreen() && maximizeMode() != MaximizeFull; +} + +void Client::updateShape() +{ + if (shape()) { + // Workaround for #19644 - Shaped windows shouldn't have decoration + if (!app_noborder) { + // Only when shape is detected for the first time, still let the user to override + app_noborder = true; + noborder = rules()->checkNoBorder(true); + updateDecoration(true); + } + if (noBorder()) { + xcb_shape_combine(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, XCB_SHAPE_SK_BOUNDING, + frameId(), clientPos().x(), clientPos().y(), window()); + } + } else if (app_noborder) { + xcb_shape_mask(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, frameId(), 0, 0, XCB_PIXMAP_NONE); + detectNoBorder(); + app_noborder = noborder; + noborder = rules()->checkNoBorder(noborder || m_motif.noBorder()); + updateDecoration(true); + } + + // Decoration mask (i.e. 'else' here) setting is done in setMask() + // when the decoration calls it or when the decoration is created/destroyed + updateInputShape(); + if (compositing()) { + addRepaintFull(); + addWorkspaceRepaint(visibleRect()); // In case shape change removes part of this window + } + emit geometryShapeChanged(this, geometry()); +} + +static Xcb::Window shape_helper_window(XCB_WINDOW_NONE); + +void Client::cleanupX11() +{ + shape_helper_window.reset(); +} + +void Client::updateInputShape() +{ + if (hiddenPreview()) // Sets it to none, don't change + return; + if (Xcb::Extensions::self()->isShapeInputAvailable()) { + // There appears to be no way to find out if a window has input + // shape set or not, so always propagate the input shape + // (it's the same like the bounding shape by default). + // Also, build the shape using a helper window, not directly + // in the frame window, because the sequence set-shape-to-frame, + // remove-shape-of-client, add-input-shape-of-client has the problem + // that after the second step there's a hole in the input shape + // until the real shape of the client is added and that can make + // the window lose focus (which is a problem with mouse focus policies) + // TODO: It seems there is, after all - XShapeGetRectangles() - but maybe this is better + if (!shape_helper_window.isValid()) + shape_helper_window.create(QRect(0, 0, 1, 1)); + shape_helper_window.resize(width(), height()); + xcb_connection_t *c = connection(); + xcb_shape_combine(c, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_SHAPE_SK_BOUNDING, + shape_helper_window, 0, 0, frameId()); + xcb_shape_combine(c, XCB_SHAPE_SO_SUBTRACT, XCB_SHAPE_SK_INPUT, XCB_SHAPE_SK_BOUNDING, + shape_helper_window, clientPos().x(), clientPos().y(), window()); + xcb_shape_combine(c, XCB_SHAPE_SO_UNION, XCB_SHAPE_SK_INPUT, XCB_SHAPE_SK_INPUT, + shape_helper_window, clientPos().x(), clientPos().y(), window()); + xcb_shape_combine(c, XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, XCB_SHAPE_SK_INPUT, + frameId(), 0, 0, shape_helper_window); + } +} + +void Client::hideClient(bool hide) +{ + if (hidden == hide) + return; + hidden = hide; + updateVisibility(); +} + +/** + * Returns whether the window is minimizable or not + */ +bool Client::isMinimizable() const +{ + if (isSpecialWindow() && !isTransient()) + return false; + if (!rules()->checkMinimize(true)) + return false; + + if (isTransient()) { + // #66868 - Let other xmms windows be minimized when the mainwindow is minimized + bool shown_mainwindow = false; + auto mainclients = mainClients(); + for (auto it = mainclients.constBegin(); + it != mainclients.constEnd(); + ++it) + if ((*it)->isShown(true)) + shown_mainwindow = true; + if (!shown_mainwindow) + return true; + } +#if 0 + // This is here because kicker's taskbar doesn't provide separate entries + // for windows with an explicitly given parent + // TODO: perhaps this should be redone + // Disabled for now, since at least modal dialogs should be minimizable + // (resulting in the mainwindow being minimized too). + if (transientFor() != NULL) + return false; +#endif + if (!wantsTabFocus()) // SELI, TODO: - NET::Utility? why wantsTabFocus() - skiptaskbar? ? + return false; + return true; +} + +void Client::doMinimize() +{ + updateVisibility(); + updateAllowedActions(); + workspace()->updateMinimizedOfTransients(this); + // Update states of all other windows in this group + if (tabGroup()) + tabGroup()->updateStates(this, TabGroup::Minimized); +} + +QRect Client::iconGeometry() const +{ + NETRect r = info->iconGeometry(); + QRect geom(r.pos.x, r.pos.y, r.size.width, r.size.height); + if (geom.isValid()) + return geom; + else { + // Check all mainwindows of this window (recursively) + foreach (AbstractClient * amainwin, mainClients()) { + Client *mainwin = dynamic_cast(amainwin); + if (!mainwin) { + continue; + } + geom = mainwin->iconGeometry(); + if (geom.isValid()) + return geom; + } + // No mainwindow (or their parents) with icon geometry was found + return AbstractClient::iconGeometry(); + } +} + +bool Client::isShadeable() const +{ + return !isSpecialWindow() && !noBorder() && (rules()->checkShade(ShadeNormal) != rules()->checkShade(ShadeNone)); +} + +void Client::setShade(ShadeMode mode) +{ + if (mode == ShadeHover && isMove()) + return; // causes geometry breaks and is probably nasty + if (isSpecialWindow() || noBorder()) + mode = ShadeNone; + mode = rules()->checkShade(mode); + if (shade_mode == mode) + return; + bool was_shade = isShade(); + ShadeMode was_shade_mode = shade_mode; + shade_mode = mode; + + // Decorations may turn off some borders when shaded + // this has to happen _before_ the tab alignment since it will restrict the minimum geometry +#if 0 + if (decoration) + decoration->borders(border_left, border_right, border_top, border_bottom); +#endif + + // Update states of all other windows in this group + if (tabGroup()) + tabGroup()->updateStates(this, TabGroup::Shaded); + + if (was_shade == isShade()) { + // Decoration may want to update after e.g. hover-shade changes + emit shadeChanged(); + return; // No real change in shaded state + } + + assert(isDecorated()); // noborder windows can't be shaded + GeometryUpdatesBlocker blocker(this); + + // TODO: All this unmapping, resizing etc. feels too much duplicated from elsewhere + if (isShade()) { + // shade_mode == ShadeNormal + addWorkspaceRepaint(visibleRect()); + // Shade + shade_geometry_change = true; + QSize s(sizeForClientSize(QSize(clientSize()))); + s.setHeight(borderTop() + borderBottom()); + m_wrapper.selectInput(ClientWinMask); // Avoid getting UnmapNotify + m_wrapper.unmap(); + m_client.unmap(); + m_wrapper.selectInput(ClientWinMask | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY); + exportMappingState(IconicState); + plainResize(s); + shade_geometry_change = false; + if (was_shade_mode == ShadeHover) { + if (shade_below && workspace()->stackingOrder().indexOf(shade_below) > -1) + workspace()->restack(this, shade_below, true); + if (isActive()) + workspace()->activateNextClient(this); + } else if (isActive()) { + workspace()->focusToNull(); + } + } else { + shade_geometry_change = true; + if (decoratedClient()) + decoratedClient()->signalShadeChange(); + QSize s(sizeForClientSize(clientSize())); + shade_geometry_change = false; + plainResize(s); + geom_restore = geometry(); + if ((shade_mode == ShadeHover || shade_mode == ShadeActivated) && rules()->checkAcceptFocus(info->input())) + setActive(true); + if (shade_mode == ShadeHover) { + ToplevelList order = workspace()->stackingOrder(); + // invalidate, since "this" could be the topmost toplevel and shade_below dangeling + shade_below = NULL; + // this is likely related to the index parameter?! + for (int idx = order.indexOf(this) + 1; idx < order.count(); ++idx) { + shade_below = qobject_cast(order.at(idx)); + if (shade_below) { + break; + } + } + if (shade_below && shade_below->isNormalWindow()) + workspace()->raiseClient(this); + else + shade_below = NULL; + } + m_wrapper.map(); + m_client.map(); + exportMappingState(NormalState); + if (isActive()) + workspace()->requestFocus(this); + } + info->setState(isShade() ? NET::Shaded : NET::States(0), NET::Shaded); + info->setState(isShown(false) ? NET::States(0) : NET::Hidden, NET::Hidden); + discardWindowPixmap(); + updateVisibility(); + updateAllowedActions(); + updateWindowRules(Rules::Shade); + + emit shadeChanged(); +} + +void Client::shadeHover() +{ + setShade(ShadeHover); + cancelShadeHoverTimer(); +} + +void Client::shadeUnhover() +{ + if (!tabGroup() || tabGroup()->current() == this || + tabGroup()->current()->shadeMode() == ShadeNormal) + setShade(ShadeNormal); + cancelShadeHoverTimer(); +} + +void Client::cancelShadeHoverTimer() +{ + delete shadeHoverTimer; + shadeHoverTimer = 0; +} + +void Client::toggleShade() +{ + // If the mode is ShadeHover or ShadeActive, cancel shade too + setShade(shade_mode == ShadeNone ? ShadeNormal : ShadeNone); +} + +void Client::updateVisibility() +{ + if (deleting) + return; + if (hidden && isCurrentTab()) { + info->setState(NET::Hidden, NET::Hidden); + setSkipTaskbar(true); // Also hide from taskbar + if (compositing() && options->hiddenPreviews() == HiddenPreviewsAlways) + internalKeep(); + else + internalHide(); + return; + } + if (isCurrentTab()) + setSkipTaskbar(originalSkipTaskbar()); // Reset from 'hidden' + if (isMinimized()) { + info->setState(NET::Hidden, NET::Hidden); + if (compositing() && options->hiddenPreviews() == HiddenPreviewsAlways) + internalKeep(); + else + internalHide(); + return; + } + info->setState(0, NET::Hidden); + if (!isOnCurrentDesktop()) { + if (compositing() && options->hiddenPreviews() != HiddenPreviewsNever) + internalKeep(); + else + internalHide(); + return; + } + if (!isOnCurrentActivity()) { + if (compositing() && options->hiddenPreviews() != HiddenPreviewsNever) + internalKeep(); + else + internalHide(); + return; + } + internalShow(); +} + + +/** + * Sets the client window's mapping state. Possible values are + * WithdrawnState, IconicState, NormalState. + */ +void Client::exportMappingState(int s) +{ + assert(m_client != XCB_WINDOW_NONE); + assert(!deleting || s == WithdrawnState); + if (s == WithdrawnState) { + m_client.deleteProperty(atoms->wm_state); + return; + } + assert(s == NormalState || s == IconicState); + + int32_t data[2]; + data[0] = s; + data[1] = XCB_NONE; + m_client.changeProperty(atoms->wm_state, atoms->wm_state, 32, 2, data); +} + +void Client::internalShow() +{ + if (mapping_state == Mapped) + return; + MappingState old = mapping_state; + mapping_state = Mapped; + if (old == Unmapped || old == Withdrawn) + map(); + if (old == Kept) { + m_decoInputExtent.map(); + updateHiddenPreview(); + } + emit windowShown(this); +} + +void Client::internalHide() +{ + if (mapping_state == Unmapped) + return; + MappingState old = mapping_state; + mapping_state = Unmapped; + if (old == Mapped || old == Kept) + unmap(); + if (old == Kept) + updateHiddenPreview(); + addWorkspaceRepaint(visibleRect()); + workspace()->clientHidden(this); + emit windowHidden(this); +} + +void Client::internalKeep() +{ + assert(compositing()); + if (mapping_state == Kept) + return; + MappingState old = mapping_state; + mapping_state = Kept; + if (old == Unmapped || old == Withdrawn) + map(); + m_decoInputExtent.unmap(); + if (isActive()) + workspace()->focusToNull(); // get rid of input focus, bug #317484 + updateHiddenPreview(); + addWorkspaceRepaint(visibleRect()); + workspace()->clientHidden(this); +} + +/** + * Maps (shows) the client. Note that it is mapping state of the frame, + * not necessarily the client window itself (i.e. a shaded window is here + * considered mapped, even though it is in IconicState). + */ +void Client::map() +{ + // XComposite invalidates backing pixmaps on unmap (minimize, different + // virtual desktop, etc.). We kept the last known good pixmap around + // for use in effects, but now we want to have access to the new pixmap + if (compositing()) + discardWindowPixmap(); + m_frame.map(); + if (!isShade()) { + m_wrapper.map(); + m_client.map(); + m_decoInputExtent.map(); + exportMappingState(NormalState); + } else + exportMappingState(IconicState); + addLayerRepaint(visibleRect()); +} + +/** + * Unmaps the client. Again, this is about the frame. + */ +void Client::unmap() +{ + // Here it may look like a race condition, as some other client might try to unmap + // the window between these two XSelectInput() calls. However, they're supposed to + // use XWithdrawWindow(), which also sends a synthetic event to the root window, + // which won't be missed, so this shouldn't be a problem. The chance the real UnmapNotify + // will be missed is also very minimal, so I don't think it's needed to grab the server + // here. + m_wrapper.selectInput(ClientWinMask); // Avoid getting UnmapNotify + m_frame.unmap(); + m_wrapper.unmap(); + m_client.unmap(); + m_decoInputExtent.unmap(); + m_wrapper.selectInput(ClientWinMask | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY); + exportMappingState(IconicState); +} + +/** + * XComposite doesn't keep window pixmaps of unmapped windows, which means + * there wouldn't be any previews of windows that are minimized or on another + * virtual desktop. Therefore rawHide() actually keeps such windows mapped. + * However special care needs to be taken so that such windows don't interfere. + * Therefore they're put very low in the stacking order and they have input shape + * set to none, which hopefully is enough. If there's no input shape available, + * then it's hoped that there will be some other desktop above it *shrug*. + * Using normal shape would be better, but that'd affect other things, e.g. painting + * of the actual preview. + */ +void Client::updateHiddenPreview() +{ + if (hiddenPreview()) { + workspace()->forceRestacking(); + if (Xcb::Extensions::self()->isShapeInputAvailable()) { + xcb_shape_rectangles(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_INPUT, + XCB_CLIP_ORDERING_UNSORTED, frameId(), 0, 0, 0, NULL); + } + } else { + workspace()->forceRestacking(); + updateInputShape(); + } +} + +void Client::sendClientMessage(xcb_window_t w, xcb_atom_t a, xcb_atom_t protocol, uint32_t data1, uint32_t data2, uint32_t data3, xcb_timestamp_t timestamp) +{ + xcb_client_message_event_t ev; + memset(&ev, 0, sizeof(ev)); + ev.response_type = XCB_CLIENT_MESSAGE; + ev.window = w; + ev.type = a; + ev.format = 32; + ev.data.data32[0] = protocol; + ev.data.data32[1] = timestamp; + ev.data.data32[2] = data1; + ev.data.data32[3] = data2; + ev.data.data32[4] = data3; + uint32_t eventMask = 0; + if (w == rootWindow()) { + eventMask = XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT; // Magic! + } + xcb_send_event(connection(), false, w, eventMask, reinterpret_cast(&ev)); + xcb_flush(connection()); +} + +/** + * Returns whether the window may be closed (have a close button) + */ +bool Client::isCloseable() const +{ + return rules()->checkCloseable(m_motif.close() && !isSpecialWindow()); +} + +/** + * Closes the window by either sending a delete_window message or using XKill. + */ +void Client::closeWindow() +{ + if (!isCloseable()) + return; + + // Update user time, because the window may create a confirming dialog. + updateUserTime(); + + if (info->supportsProtocol(NET::DeleteWindowProtocol)) { + sendClientMessage(window(), atoms->wm_protocols, atoms->wm_delete_window); + pingWindow(); + } else // Client will not react on wm_delete_window. We have not choice + // but destroy his connection to the XServer. + killWindow(); +} + + +/** + * Kills the window via XKill + */ +void Client::killWindow() +{ + qCDebug(KWIN_CORE) << "Client::killWindow():" << caption(); + killProcess(false); + m_client.kill(); // Always kill this client at the server + destroyClient(); +} + +/** + * Send a ping to the window using _NET_WM_PING if possible if it + * doesn't respond within a reasonable time, it will be killed. + */ +void Client::pingWindow() +{ + if (!info->supportsProtocol(NET::PingProtocol)) + return; // Can't ping :( + if (options->killPingTimeout() == 0) + return; // Turned off + if (ping_timer != NULL) + return; // Pinging already + ping_timer = new QTimer(this); + connect(ping_timer, &QTimer::timeout, this, + [this]() { + if (unresponsive()) { + qCDebug(KWIN_CORE) << "Final ping timeout, asking to kill:" << caption(); + ping_timer->deleteLater(); + ping_timer = nullptr; + killProcess(true, m_pingTimestamp); + return; + } + + qCDebug(KWIN_CORE) << "First ping timeout:" << caption(); + + setUnresponsive(true); + ping_timer->start(); + } + ); + ping_timer->setSingleShot(true); + // we'll run the timer twice, at first we'll desaturate the window + // and the second time we'll show the "do you want to kill" prompt + ping_timer->start(options->killPingTimeout() / 2); + m_pingTimestamp = xTime(); + workspace()->sendPingToWindow(window(), m_pingTimestamp); +} + +void Client::gotPing(xcb_timestamp_t timestamp) +{ + // Just plain compare is not good enough because of 64bit and truncating and whatnot + if (NET::timestampCompare(timestamp, m_pingTimestamp) != 0) + return; + delete ping_timer; + ping_timer = NULL; + + setUnresponsive(false); + + if (m_killHelperPID && !::kill(m_killHelperPID, 0)) { // means the process is alive + ::kill(m_killHelperPID, SIGTERM); + m_killHelperPID = 0; + } +} + +void Client::killProcess(bool ask, xcb_timestamp_t timestamp) +{ + if (m_killHelperPID && !::kill(m_killHelperPID, 0)) // means the process is alive + return; + Q_ASSERT(!ask || timestamp != XCB_TIME_CURRENT_TIME); + pid_t pid = info->pid(); + if (pid <= 0 || clientMachine()->hostName().isEmpty()) // Needed properties missing + return; + qCDebug(KWIN_CORE) << "Kill process:" << pid << "(" << clientMachine()->hostName() << ")"; + if (!ask) { + if (!clientMachine()->isLocal()) { + QStringList lst; + lst << QString::fromUtf8(clientMachine()->hostName()) << QStringLiteral("kill") << QString::number(pid); + QProcess::startDetached(QStringLiteral("xon"), lst); + } else + ::kill(pid, SIGTERM); + } else { + QString hostname = clientMachine()->isLocal() ? QStringLiteral("localhost") : QString::fromUtf8(clientMachine()->hostName()); + QProcess::startDetached(QStringLiteral(KWIN_KILLER_BIN), + QStringList() << QStringLiteral("--pid") << QString::number(unsigned(pid)) << QStringLiteral("--hostname") << hostname + << QStringLiteral("--windowname") << captionNormal() + << QStringLiteral("--applicationname") << QString::fromUtf8(resourceClass()) + << QStringLiteral("--wid") << QString::number(window()) + << QStringLiteral("--timestamp") << QString::number(timestamp), + QString(), &m_killHelperPID); + } +} + +void Client::doSetSkipTaskbar() +{ + info->setState(skipTaskbar() ? NET::SkipTaskbar : NET::States(0), NET::SkipTaskbar); +} + +void Client::doSetSkipPager() +{ + info->setState(skipPager() ? NET::SkipPager : NET::States(0), NET::SkipPager); +} + +void Client::doSetSkipSwitcher() +{ + info->setState(skipSwitcher() ? NET::SkipSwitcher : NET::States(0), NET::SkipSwitcher); +} + +void Client::doSetDesktop(int desktop, int was_desk) +{ + Q_UNUSED(desktop) + Q_UNUSED(was_desk) + updateVisibility(); + + // Update states of all other windows in this group + if (tabGroup()) + tabGroup()->updateStates(this, TabGroup::Desktop); +} + +/** + * Sets whether the client is on @p activity. + * If you remove it from its last activity, then it's on all activities. + * + * Note: If it was on all activities and you try to remove it from one, nothing will happen; + * I don't think that's an important enough use case to handle here. + */ +void Client::setOnActivity(const QString &activity, bool enable) +{ +#ifdef KWIN_BUILD_ACTIVITIES + if (! Activities::self()) { + return; + } + QStringList newActivitiesList = activities(); + if (newActivitiesList.contains(activity) == enable) //nothing to do + return; + if (enable) { + QStringList allActivities = Activities::self()->all(); + if (!allActivities.contains(activity)) //bogus ID + return; + newActivitiesList.append(activity); + } else + newActivitiesList.removeOne(activity); + setOnActivities(newActivitiesList); +#else + Q_UNUSED(activity) + Q_UNUSED(enable) +#endif +} + +/** + * set exactly which activities this client is on + */ +void Client::setOnActivities(QStringList newActivitiesList) +{ +#ifdef KWIN_BUILD_ACTIVITIES + if (!Activities::self()) { + return; + } + QString joinedActivitiesList = newActivitiesList.join(QStringLiteral(",")); + joinedActivitiesList = rules()->checkActivity(joinedActivitiesList, false); + newActivitiesList = joinedActivitiesList.split(u',', QString::SkipEmptyParts); + + QStringList allActivities = Activities::self()->all(); + + auto it = newActivitiesList.begin(); + while (it != newActivitiesList.end()) { + if (! allActivities.contains(*it)) { + it = newActivitiesList.erase(it); + } else { + it++; + } + } + + if (// If we got the request to be on all activities explicitly + newActivitiesList.isEmpty() || joinedActivitiesList == Activities::nullUuid() || + // If we got a list of activities that covers all activities + (newActivitiesList.count() > 1 && newActivitiesList.count() == allActivities.count())) { + + activityList.clear(); + const QByteArray nullUuid = Activities::nullUuid().toUtf8(); + m_client.changeProperty(atoms->activities, XCB_ATOM_STRING, 8, nullUuid.length(), nullUuid.constData()); + + } else { + QByteArray joined = joinedActivitiesList.toAscii(); + activityList = newActivitiesList; + m_client.changeProperty(atoms->activities, XCB_ATOM_STRING, 8, joined.length(), joined.constData()); + } + + updateActivities(false); +#else + Q_UNUSED(newActivitiesList) +#endif +} + +void Client::blockActivityUpdates(bool b) +{ + if (b) { + ++m_activityUpdatesBlocked; + } else { + Q_ASSERT(m_activityUpdatesBlocked); + --m_activityUpdatesBlocked; + if (!m_activityUpdatesBlocked) + updateActivities(m_blockedActivityUpdatesRequireTransients); + } +} + +/** + * update after activities changed + */ +void Client::updateActivities(bool includeTransients) +{ + if (m_activityUpdatesBlocked) { + m_blockedActivityUpdatesRequireTransients |= includeTransients; + return; + } + emit activitiesChanged(this); + m_blockedActivityUpdatesRequireTransients = false; // reset + FocusChain::self()->update(this, FocusChain::MakeFirst); + updateVisibility(); + updateWindowRules(Rules::Activity); + + // Update states of all other windows in this group + if (tabGroup()) + tabGroup()->updateStates(this, TabGroup::Activity); +} + +/** + * Returns the list of activities the client window is on. + * if it's on all activities, the list will be empty. + * Don't use this, use isOnActivity() and friends (from class Toplevel) + */ +QStringList Client::activities() const +{ + if (sessionActivityOverride) { + return QStringList(); + } + return activityList; +} + +/** + * if @p on is true, sets on all activities. + * if it's false, sets it to only be on the current activity + */ +void Client::setOnAllActivities(bool on) +{ +#ifdef KWIN_BUILD_ACTIVITIES + if (on == isOnAllActivities()) + return; + if (on) { + setOnActivities(QStringList()); + + } else { + setOnActivity(Activities::self()->current(), true); + } +#else + Q_UNUSED(on) +#endif +} + +/** + * Performs the actual focusing of the window using XSetInputFocus and WM_TAKE_FOCUS + */ +void Client::takeFocus() +{ + if (rules()->checkAcceptFocus(info->input())) + m_client.focus(); + else + demandAttention(false); // window cannot take input, at least withdraw urgency + if (info->supportsProtocol(NET::TakeFocusProtocol)) { + sendClientMessage(window(), atoms->wm_protocols, atoms->wm_take_focus, 0, 0, 0, XCB_CURRENT_TIME); + } + workspace()->setShouldGetFocus(this); + + bool breakShowingDesktop = !keepAbove(); + if (breakShowingDesktop) { + foreach (const Client *c, group()->members()) { + if (c->isDesktop()) { + breakShowingDesktop = false; + break; + } + } + } + if (breakShowingDesktop) + workspace()->setShowingDesktop(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. + * + * \sa contextHelp() + */ +bool Client::providesContextHelp() const +{ + return info->supportsProtocol(NET::ContextHelpProtocol); +} + +/** + * Invokes context help on the window. Only works if the window + * actually provides context help. + * + * \sa providesContextHelp() + */ +void Client::showContextHelp() +{ + if (info->supportsProtocol(NET::ContextHelpProtocol)) { + sendClientMessage(window(), atoms->wm_protocols, atoms->net_wm_context_help); + } +} + +/** + * Fetches the window's caption (WM_NAME property). It will be + * stored in the client's caption(). + */ +void Client::fetchName() +{ + setCaption(readName()); +} + +static inline QString readNameProperty(xcb_window_t w, xcb_atom_t atom) +{ + const auto cookie = xcb_icccm_get_text_property_unchecked(connection(), w, atom); + xcb_icccm_get_text_property_reply_t reply; + if (xcb_icccm_get_wm_name_reply(connection(), cookie, &reply, nullptr)) { + QString retVal; + if (reply.encoding == atoms->utf8_string) { + retVal = QString::fromUtf8(QByteArray(reply.name, reply.name_len)); + } else if (reply.encoding == XCB_ATOM_STRING) { + retVal = QString::fromLocal8Bit(QByteArray(reply.name, reply.name_len)); + } + xcb_icccm_get_text_property_reply_wipe(&reply); + return retVal.simplified(); + } + return QString(); +} + +QString Client::readName() const +{ + if (info->name() && info->name()[0] != '\0') + return QString::fromUtf8(info->name()).simplified(); + else { + return readNameProperty(window(), XCB_ATOM_WM_NAME); + } +} + +// The list is taken from http://www.unicode.org/reports/tr9/ (#154840) +static const QChar LRM(0x200E); + +void Client::setCaption(const QString& _s, bool force) +{ + if (!force && _s == cap_normal) + return; + QString s(_s); + for (int i = 0; i < s.length(); ++i) + if (!s[i].isPrint()) + s[i] = QChar(u' '); + const bool changed = (s != cap_normal); + cap_normal = s; + if (!force && !changed) { + emit captionChanged(); + return; + } + + bool reset_name = force; + bool was_suffix = (!cap_suffix.isEmpty()); + cap_suffix.clear(); + QString machine_suffix; + if (!options->condensedTitle()) { // machine doesn't qualify for "clean" + if (clientMachine()->hostName() != ClientMachine::localhost() && !clientMachine()->isLocal()) + machine_suffix = QLatin1String(" <@") + QString::fromUtf8(clientMachine()->hostName()) + QLatin1Char('>') + LRM; + } + QString shortcut_suffix = shortcutCaptionSuffix(); + cap_suffix = machine_suffix + shortcut_suffix; + if ((!isSpecialWindow() || isToolbar()) && findClientWithSameCaption()) { + int i = 2; + do { + cap_suffix = machine_suffix + QLatin1String(" <") + QString::number(i) + QLatin1Char('>') + LRM; + i++; + } while (findClientWithSameCaption()); + info->setVisibleName(caption().toUtf8().constData()); + reset_name = false; + } + if ((was_suffix && cap_suffix.isEmpty()) || reset_name) { + // If it was new window, it may have old value still set, if the window is reused + info->setVisibleName(""); + info->setVisibleIconName(""); + } else if (!cap_suffix.isEmpty() && !cap_iconic.isEmpty()) + // Keep the same suffix in iconic name if it's set + info->setVisibleIconName(QString(cap_iconic + cap_suffix).toUtf8().constData()); + + emit captionChanged(); +} + +void Client::updateCaption() +{ + setCaption(cap_normal, true); +} + +void Client::fetchIconicName() +{ + QString s; + if (info->iconName() && info->iconName()[0] != '\0') + s = QString::fromUtf8(info->iconName()); + else + s = readNameProperty(window(), XCB_ATOM_WM_ICON_NAME); + if (s != cap_iconic) { + bool was_set = !cap_iconic.isEmpty(); + cap_iconic = s; + if (!cap_suffix.isEmpty()) { + if (!cap_iconic.isEmpty()) // Keep the same suffix in iconic name if it's set + info->setVisibleIconName(QString(s + cap_suffix).toUtf8().constData()); + else if (was_set) + info->setVisibleIconName(""); + } + } +} + +void Client::setClientShown(bool shown) +{ + if (deleting) + return; // Don't change shown status if this client is being deleted + if (shown != hidden) + return; // nothing to change + hidden = !shown; + if (options->isInactiveTabsSkipTaskbar()) + setSkipTaskbar(hidden); // TODO: Causes reshuffle of the taskbar + if (shown) { + map(); + takeFocus(); + autoRaise(); + FocusChain::self()->update(this, FocusChain::MakeFirst); + } else { + unmap(); + // Don't move tabs to the end of the list when another tab get's activated + if (isCurrentTab()) + FocusChain::self()->update(this, FocusChain::MakeLast); + addWorkspaceRepaint(visibleRect()); + } +} + +void Client::getMotifHints() +{ + const bool wasClosable = m_motif.close(); + const bool wasNoBorder = m_motif.noBorder(); + if (m_managed) // only on property change, initial read is prefetched + m_motif.fetch(); + m_motif.read(); + if (m_motif.hasDecoration() && m_motif.noBorder() != wasNoBorder) { + // If we just got a hint telling us to hide decorations, we do so. + if (m_motif.noBorder()) + noborder = rules()->checkNoBorder(true); + // If the Motif hint is now telling us to show decorations, we only do so if the app didn't + // instruct us to hide decorations in some other way, though. + else if (!app_noborder) + noborder = rules()->checkNoBorder(false); + } + + // mminimize; - Ignore, bogus - E.g. shading or sending to another desktop is "minimizing" too + // mmaximize; - Ignore, bogus - Maximizing is basically just resizing + const bool closabilityChanged = wasClosable != m_motif.close(); + if (isManaged()) + updateDecoration(true); // Check if noborder state has changed + if (closabilityChanged) { + emit closeableChanged(isCloseable()); + } +} + +void Client::getIcons() +{ + // First read icons from the window itself + const QString themedIconName = iconFromDesktopFile(); + if (!themedIconName.isEmpty()) { + setIcon(QIcon::fromTheme(themedIconName)); + return; + } + QIcon icon; + auto readIcon = [this, &icon](int size, bool scale = true) { + const QPixmap pix = KWindowSystem::icon(window(), size, size, scale, KWindowSystem::NETWM | KWindowSystem::WMHints, info); + if (!pix.isNull()) { + icon.addPixmap(pix); + } + }; + readIcon(16); + readIcon(32); + readIcon(48, false); + readIcon(64, false); + readIcon(128, false); + if (icon.isNull()) { + // Then try window group + icon = group()->icon(); + } + if (icon.isNull() && isTransient()) { + // Then mainclients + auto mainclients = mainClients(); + for (auto it = mainclients.constBegin(); + it != mainclients.constEnd() && icon.isNull(); + ++it) { + if (!(*it)->icon().isNull()) { + icon = (*it)->icon(); + break; + } + } + } + if (icon.isNull()) { + // And if nothing else, load icon from classhint or xapp icon + icon.addPixmap(KWindowSystem::icon(window(), 32, 32, true, KWindowSystem::ClassHint | KWindowSystem::XApp, info)); + icon.addPixmap(KWindowSystem::icon(window(), 16, 16, true, KWindowSystem::ClassHint | KWindowSystem::XApp, info)); + icon.addPixmap(KWindowSystem::icon(window(), 64, 64, false, KWindowSystem::ClassHint | KWindowSystem::XApp, info)); + icon.addPixmap(KWindowSystem::icon(window(), 128, 128, false, KWindowSystem::ClassHint | KWindowSystem::XApp, info)); + } + setIcon(icon); +} + +void Client::getSyncCounter() +{ + // TODO: make sync working on XWayland + static const bool isX11 = kwinApp()->operationMode() == Application::OperationModeX11; + if (!Xcb::Extensions::self()->isSyncAvailable() || !isX11) + return; + + Xcb::Property syncProp(false, window(), atoms->net_wm_sync_request_counter, XCB_ATOM_CARDINAL, 0, 1); + const xcb_sync_counter_t counter = syncProp.value(XCB_NONE); + if (counter != XCB_NONE) { + syncRequest.counter = counter; + syncRequest.value.hi = 0; + syncRequest.value.lo = 0; + auto *c = connection(); + xcb_sync_set_counter(c, syncRequest.counter, syncRequest.value); + if (syncRequest.alarm == XCB_NONE) { + const uint32_t mask = XCB_SYNC_CA_COUNTER | XCB_SYNC_CA_VALUE_TYPE | XCB_SYNC_CA_TEST_TYPE | XCB_SYNC_CA_EVENTS; + const uint32_t values[] = { + syncRequest.counter, + XCB_SYNC_VALUETYPE_RELATIVE, + XCB_SYNC_TESTTYPE_POSITIVE_TRANSITION, + 1 + }; + syncRequest.alarm = xcb_generate_id(c); + auto cookie = xcb_sync_create_alarm_checked(c, syncRequest.alarm, mask, values); + ScopedCPointer error(xcb_request_check(c, cookie)); + if (!error.isNull()) { + syncRequest.alarm = XCB_NONE; + } else { + xcb_sync_change_alarm_value_list_t value; + memset(&value, 0, sizeof(value)); + value.value.hi = 0; + value.value.lo = 1; + value.delta.hi = 0; + value.delta.lo = 1; + xcb_sync_change_alarm_aux(c, syncRequest.alarm, XCB_SYNC_CA_DELTA | XCB_SYNC_CA_VALUE, &value); + } + } + } +} + +/** + * Send the client a _NET_SYNC_REQUEST + */ +void Client::sendSyncRequest() +{ + if (syncRequest.counter == XCB_NONE || syncRequest.isPending) + return; // do NOT, NEVER send a sync request when there's one on the stack. the clients will just stop respoding. FOREVER! ... + + if (!syncRequest.failsafeTimeout) { + syncRequest.failsafeTimeout = new QTimer(this); + connect(syncRequest.failsafeTimeout, &QTimer::timeout, this, + [this]() { + // client does not respond to XSYNC requests in reasonable time, remove support + if (!ready_for_painting) { + // failed on initial pre-show request + setReadyForPainting(); + setupWindowManagementInterface(); + return; + } + // failed during resize + syncRequest.isPending = false; + syncRequest.counter = syncRequest.alarm = XCB_NONE; + delete syncRequest.timeout; delete syncRequest.failsafeTimeout; + syncRequest.timeout = syncRequest.failsafeTimeout = nullptr; + syncRequest.lastTimestamp = XCB_CURRENT_TIME; + } + ); + syncRequest.failsafeTimeout->setSingleShot(true); + } + // if there's no response within 10 seconds, sth. went wrong and we remove XSYNC support from this client. + // see events.cpp Client::syncEvent() + syncRequest.failsafeTimeout->start(ready_for_painting ? 10000 : 1000); + + // We increment before the notify so that after the notify + // syncCounterSerial will equal the value we are expecting + // in the acknowledgement + const uint32_t oldLo = syncRequest.value.lo; + syncRequest.value.lo++;; + if (oldLo > syncRequest.value.lo) { + syncRequest.value.hi++; + } + if (syncRequest.lastTimestamp >= xTime()) { + updateXTime(); + } + + // Send the message to client + sendClientMessage(window(), atoms->wm_protocols, atoms->net_wm_sync_request, syncRequest.value.lo, syncRequest.value.hi); + syncRequest.isPending = true; + syncRequest.lastTimestamp = xTime(); +} + +bool Client::wantsInput() const +{ + return rules()->checkAcceptFocus(acceptsFocus() || info->supportsProtocol(NET::TakeFocusProtocol)); +} + +bool Client::acceptsFocus() const +{ + return info->input(); +} + +void Client::setBlockingCompositing(bool block) +{ + const bool usedToBlock = blocks_compositing; + blocks_compositing = rules()->checkBlockCompositing(block && options->windowsBlockCompositing()); + if (usedToBlock != blocks_compositing) { + emit blockingCompositingChanged(blocks_compositing ? this : 0); + } +} + +void Client::updateAllowedActions(bool force) +{ + if (!isManaged() && !force) + return; + NET::Actions old_allowed_actions = NET::Actions(allowed_actions); + allowed_actions = 0; + if (isMovable()) + allowed_actions |= NET::ActionMove; + if (isResizable()) + allowed_actions |= NET::ActionResize; + if (isMinimizable()) + allowed_actions |= NET::ActionMinimize; + if (isShadeable()) + allowed_actions |= NET::ActionShade; + // Sticky state not supported + if (isMaximizable()) + allowed_actions |= NET::ActionMax; + if (userCanSetFullScreen()) + allowed_actions |= NET::ActionFullScreen; + allowed_actions |= NET::ActionChangeDesktop; // Always (Pagers shouldn't show Docks etc.) + if (isCloseable()) + allowed_actions |= NET::ActionClose; + if (old_allowed_actions == allowed_actions) + return; + // TODO: This could be delayed and compressed - It's only for pagers etc. anyway + info->setAllowedActions(allowed_actions); + + // ONLY if relevant features have changed (and the window didn't just get/loose moveresize for maximization state changes) + const NET::Actions relevant = ~(NET::ActionMove|NET::ActionResize); + if ((allowed_actions & relevant) != (old_allowed_actions & relevant)) { + if ((allowed_actions & NET::ActionMinimize) != (old_allowed_actions & NET::ActionMinimize)) { + emit minimizeableChanged(allowed_actions & NET::ActionMinimize); + } + if ((allowed_actions & NET::ActionShade) != (old_allowed_actions & NET::ActionShade)) { + emit shadeableChanged(allowed_actions & NET::ActionShade); + } + if ((allowed_actions & NET::ActionMax) != (old_allowed_actions & NET::ActionMax)) { + emit maximizeableChanged(allowed_actions & NET::ActionMax); + } + } +} + +void Client::debug(QDebug& stream) const +{ + print(stream); +} + +Xcb::StringProperty Client::fetchActivities() const +{ +#ifdef KWIN_BUILD_ACTIVITIES + return Xcb::StringProperty(window(), atoms->activities); +#else + return Xcb::StringProperty(); +#endif +} + +void Client::readActivities(Xcb::StringProperty &property) +{ +#ifdef KWIN_BUILD_ACTIVITIES + QStringList newActivitiesList; + QString prop = QString::fromUtf8(property); + activitiesDefined = !prop.isEmpty(); + + if (prop == Activities::nullUuid()) { + //copied from setOnAllActivities to avoid a redundant XChangeProperty. + if (!activityList.isEmpty()) { + activityList.clear(); + updateActivities(true); + } + return; + } + if (prop.isEmpty()) { + //note: this makes it *act* like it's on all activities but doesn't set the property to 'ALL' + if (!activityList.isEmpty()) { + activityList.clear(); + updateActivities(true); + } + return; + } + + newActivitiesList = prop.split(u','); + + if (newActivitiesList == activityList) + return; //expected change, it's ok. + + //otherwise, somebody else changed it. we need to validate before reacting. + //if the activities are not synced, and there are existing clients with + //activities specified, somebody has restarted kwin. we can not validate + //activities in this case. we need to trust the old values. + if (Activities::self() && Activities::self()->serviceStatus() != KActivities::Consumer::Unknown) { + QStringList allActivities = Activities::self()->all(); + if (allActivities.isEmpty()) { + qCDebug(KWIN_CORE) << "no activities!?!?"; + //don't touch anything, there's probably something bad going on and we don't wanna make it worse + return; + } + + + for (int i = 0; i < newActivitiesList.size(); ++i) { + if (! allActivities.contains(newActivitiesList.at(i))) { + qCDebug(KWIN_CORE) << "invalid:" << newActivitiesList.at(i); + newActivitiesList.removeAt(i--); + } + } + } + setOnActivities(newActivitiesList); +#else + Q_UNUSED(property) +#endif +} + +void Client::checkActivities() +{ +#ifdef KWIN_BUILD_ACTIVITIES + Xcb::StringProperty property = fetchActivities(); + readActivities(property); +#endif +} + +void Client::setSessionActivityOverride(bool needed) +{ + sessionActivityOverride = needed; + updateActivities(false); +} + +QRect Client::decorationRect() const +{ + return QRect(0, 0, width(), height()); +} + +Xcb::Property Client::fetchFirstInTabBox() const +{ + return Xcb::Property(false, m_client, atoms->kde_first_in_window_list, + atoms->kde_first_in_window_list, 0, 1); +} + +void Client::readFirstInTabBox(Xcb::Property &property) +{ + setFirstInTabBox(property.toBool(32, atoms->kde_first_in_window_list)); +} + +void Client::updateFirstInTabBox() +{ + // TODO: move into KWindowInfo + Xcb::Property property = fetchFirstInTabBox(); + readFirstInTabBox(property); +} + +Xcb::StringProperty Client::fetchColorScheme() const +{ + return Xcb::StringProperty(m_client, atoms->kde_color_sheme); +} + +void Client::readColorScheme(Xcb::StringProperty &property) +{ + AbstractClient::updateColorScheme(rules()->checkDecoColor(QString::fromUtf8(property))); +} + +void Client::updateColorScheme() +{ + Xcb::StringProperty property = fetchColorScheme(); + readColorScheme(property); +} + +bool Client::isClient() const +{ + return true; +} + +NET::WindowType Client::windowType(bool direct, int supportedTypes) const +{ + // TODO: does it make sense to cache the returned window type for SUPPORTED_MANAGED_WINDOW_TYPES_MASK? + if (supportedTypes == 0) { + supportedTypes = SUPPORTED_MANAGED_WINDOW_TYPES_MASK; + } + NET::WindowType wt = info->windowType(NET::WindowTypes(supportedTypes)); + if (direct) { + return wt; + } + NET::WindowType wt2 = rules()->checkType(wt); + if (wt != wt2) { + wt = wt2; + info->setWindowType(wt); // force hint change + } + // hacks here + if (wt == NET::Unknown) // this is more or less suggested in NETWM spec + wt = isTransient() ? NET::Dialog : NET::Normal; + return wt; +} + +void Client::cancelFocusOutTimer() +{ + if (m_focusOutTimer) { + m_focusOutTimer->stop(); + } +} + +xcb_window_t Client::frameId() const +{ + return m_frame; +} + +Xcb::Property Client::fetchShowOnScreenEdge() const +{ + return Xcb::Property(false, window(), atoms->kde_screen_edge_show, XCB_ATOM_CARDINAL, 0, 1); +} + +void Client::readShowOnScreenEdge(Xcb::Property &property) +{ + //value comes in two parts, edge in the lower byte + //then the type in the upper byte + // 0 = autohide + // 1 = raise in front on activate + + const uint32_t value = property.value(ElectricNone); + ElectricBorder border = ElectricNone; + switch (value & 0xFF) { + case 0: + border = ElectricTop; + break; + case 1: + border = ElectricRight; + break; + case 2: + border = ElectricBottom; + break; + case 3: + border = ElectricLeft; + break; + } + if (border != ElectricNone) { + disconnect(m_edgeRemoveConnection); + disconnect(m_edgeGeometryTrackingConnection); + bool successfullyHidden = false; + + if (((value >> 8) & 0xFF) == 1) { + setKeepBelow(true); + successfullyHidden = keepBelow(); //request could have failed due to user kwin rules + + m_edgeRemoveConnection = connect(this, &AbstractClient::keepBelowChanged, this, [this](){ + if (!keepBelow()) { + ScreenEdges::self()->reserve(this, ElectricNone); + } + }); + } else { + hideClient(true); + successfullyHidden = isHiddenInternal(); + + m_edgeGeometryTrackingConnection = connect(this, &Client::geometryChanged, this, [this, border](){ + hideClient(true); + ScreenEdges::self()->reserve(this, border); + }); + } + + if (successfullyHidden) { + ScreenEdges::self()->reserve(this, border); + } else { + ScreenEdges::self()->reserve(this, ElectricNone); + } + } else if (!property.isNull() && property->type != XCB_ATOM_NONE) { + // property value is incorrect, delete the property + // so that the client knows that it is not hidden + xcb_delete_property(connection(), window(), atoms->kde_screen_edge_show); + } else { + // restore + // TODO: add proper unreserve + + //this will call showOnScreenEdge to reset the state + disconnect(m_edgeGeometryTrackingConnection); + ScreenEdges::self()->reserve(this, ElectricNone); + } +} + +void Client::updateShowOnScreenEdge() +{ + Xcb::Property property = fetchShowOnScreenEdge(); + readShowOnScreenEdge(property); +} + +void Client::showOnScreenEdge() +{ + disconnect(m_edgeRemoveConnection); + + hideClient(false); + setKeepBelow(false); + xcb_delete_property(connection(), window(), atoms->kde_screen_edge_show); +} + +void Client::addDamage(const QRegion &damage) +{ + if (!ready_for_painting) { // avoid "setReadyForPainting()" function calling overhead + if (syncRequest.counter == XCB_NONE) { // cannot detect complete redraw, consider done now + setReadyForPainting(); + setupWindowManagementInterface(); + } + } + repaints_region += damage; + Toplevel::addDamage(damage); +} + +bool Client::belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const +{ + const Client *c2 = dynamic_cast(other); + if (!c2) { + return false; + } + return Client::belongToSameApplication(this, c2, checks); +} + +void Client::updateTabGroupStates(TabGroup::States states) +{ + if (auto t = tabGroup()) { + t->updateStates(this, states); + } +} + +QSize Client::resizeIncrements() const +{ + return m_geometryHints.resizeIncrements(); +} + +Xcb::StringProperty Client::fetchApplicationMenuServiceName() const +{ + return Xcb::StringProperty(m_client, atoms->kde_net_wm_appmenu_service_name); +} + +void Client::readApplicationMenuServiceName(Xcb::StringProperty &property) +{ + updateApplicationMenuServiceName(QString::fromUtf8(property)); +} + +void Client::checkApplicationMenuServiceName() +{ + Xcb::StringProperty property = fetchApplicationMenuServiceName(); + readApplicationMenuServiceName(property); +} + +Xcb::StringProperty Client::fetchApplicationMenuObjectPath() const +{ + return Xcb::StringProperty(m_client, atoms->kde_net_wm_appmenu_object_path); +} + +void Client::readApplicationMenuObjectPath(Xcb::StringProperty &property) +{ + updateApplicationMenuObjectPath(QString::fromUtf8(property)); +} + +void Client::checkApplicationMenuObjectPath() +{ + Xcb::StringProperty property = fetchApplicationMenuObjectPath(); + readApplicationMenuObjectPath(property); +} + +void Client::handleSync() +{ + setReadyForPainting(); + setupWindowManagementInterface(); + syncRequest.isPending = false; + if (syncRequest.failsafeTimeout) + syncRequest.failsafeTimeout->stop(); + if (isResize()) { + if (syncRequest.timeout) + syncRequest.timeout->stop(); + performMoveResize(); + } else // setReadyForPainting does as well, but there's a small chance for resize syncs after the resize ended + addRepaintFull(); +} + +} // namespace + diff --git a/client.h b/client.h new file mode 100644 index 0000000..fcc7288 --- /dev/null +++ b/client.h @@ -0,0 +1,700 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ + +#ifndef KWIN_CLIENT_H +#define KWIN_CLIENT_H + +// kwin +#include "options.h" +#include "rules.h" +#include "tabgroup.h" +#include "abstract_client.h" +#include "xcbutils.h" +// Qt +#include +#include +#include +#include +#include +// X +#include + +// TODO: Cleanup the order of things in this .h file + +class QTimer; +class KStartupInfoData; +class KStartupInfoId; + +namespace KWin +{ + + +/** + * @brief Defines Predicates on how to search for a Client. + * + * Used by Workspace::findClient. + */ +enum class Predicate { + WindowMatch, + WrapperIdMatch, + FrameIdMatch, + InputIdMatch +}; + +class KWIN_EXPORT Client + : public AbstractClient +{ + Q_OBJECT + /** + * 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. + * The value is evaluated each time the getter is called. + * Because of that no changed signal is provided. + */ + Q_PROPERTY(QSize basicUnit READ basicUnit) + /** + * A client can block compositing. That is while the Client is alive and the state is set, + * Compositing is suspended and is resumed when there are no Clients blocking compositing any + * more. + * + * This is actually set by a window property, unfortunately not used by the target application + * group. For convenience it's exported as a property to the scripts. + * + * Use with care! + **/ + Q_PROPERTY(bool blocksCompositing READ isBlockingCompositing WRITE setBlockingCompositing NOTIFY blockingCompositingChanged) + /** + * Whether the Client uses client side window decorations. + * Only GTK+ are detected. + **/ + Q_PROPERTY(bool clientSideDecorated READ isClientSideDecorated NOTIFY clientSideDecoratedChanged) +public: + explicit Client(); + xcb_window_t wrapperId() const; + xcb_window_t inputId() const { return m_decoInputExtent; } + virtual xcb_window_t frameId() const override; + + bool isTransient() const override; + bool groupTransient() const; + bool wasOriginallyGroupTransient() const; + QList mainClients() const override; // Call once before loop , is not indirect + bool hasTransient(const AbstractClient* c, bool indirect) const override; + void checkTransient(xcb_window_t w); + AbstractClient* findModal(bool allow_itself = false) override; + const Group* group() const; + Group* group(); + void checkGroup(Group* gr = NULL, bool force = false); + void changeClientLeaderGroup(Group* gr); + void updateWindowRules(Rules::Types selection) override; + void updateFullscreenMonitors(NETFullscreenMonitors topology); + + bool hasNETSupport() const; + + QSize minSize() const override; + QSize maxSize() const override; + QSize basicUnit() const; + virtual QSize clientSize() const; + QPoint inputPos() const { return input_offset; } // Inside of geometry() + + bool windowEvent(xcb_generic_event_t *e); + NET::WindowType windowType(bool direct = false, int supported_types = 0) const; + + bool manage(xcb_window_t w, bool isMapped); + void releaseWindow(bool on_shutdown = false); + void destroyClient(); + + virtual QStringList activities() const; + void setOnActivity(const QString &activity, bool enable); + void setOnAllActivities(bool set) override; + void setOnActivities(QStringList newActivitiesList) override; + void updateActivities(bool includeTransients); + void blockActivityUpdates(bool b = true) override; + + /// Is not minimized and not hidden. I.e. normally visible on some virtual desktop. + bool isShown(bool shaded_is_shown) const override; + bool isHiddenInternal() const override; // For compositing + + ShadeMode shadeMode() const override; // Prefer isShade() + void setShade(ShadeMode mode) override; + bool isShadeable() const override; + + bool isMaximizable() const override; + QRect geometryRestore() const override; + MaximizeMode maximizeMode() const override; + + bool isMinimizable() const override; + QRect iconGeometry() const override; + + void setFullScreen(bool set, bool user = true) override; + bool isFullScreen() const override; + bool userCanSetFullScreen() const override; + QRect geometryFSRestore() const { + return geom_fs_restore; // Only for session saving + } + int fullScreenMode() const { + return fullscreen_mode; // only for session saving + } + + bool noBorder() const override; + void setNoBorder(bool set) override; + bool userCanSetNoBorder() const override; + void checkNoBorder() override; + + int sessionStackingOrder() const; + + // Auxiliary functions, depend on the windowType + bool wantsInput() const override; + + bool isResizable() const override; + bool isMovable() const override; + bool isMovableAcrossScreens() const override; + bool isCloseable() const override; ///< May be closed by the user (May have a close button) + + void takeFocus() override; + + void updateDecoration(bool check_workspace_pos, bool force = false) override; + + void updateShape(); + + using AbstractClient::setGeometry; + void setGeometry(int x, int y, int w, int h, ForceGeometry_t force = NormalGeometrySet) override; + /// plainResize() simply resizes + void plainResize(int w, int h, ForceGeometry_t force = NormalGeometrySet); + void plainResize(const QSize& s, ForceGeometry_t force = NormalGeometrySet); + /// resizeWithChecks() resizes according to gravity, and checks workarea position + using AbstractClient::resizeWithChecks; + void resizeWithChecks(int w, int h, ForceGeometry_t force = NormalGeometrySet) override; + void resizeWithChecks(int w, int h, xcb_gravity_t gravity, ForceGeometry_t force = NormalGeometrySet); + void resizeWithChecks(const QSize& s, xcb_gravity_t gravity, ForceGeometry_t force = NormalGeometrySet); + QSize sizeForClientSize(const QSize&, Sizemode mode = SizemodeAny, bool noframe = false) const override; + + bool providesContextHelp() const override; + + Options::WindowOperation mouseButtonToWindowOperation(Qt::MouseButtons button); + bool performMouseCommand(Options::MouseCommand, const QPoint& globalPos) override; + + QRect adjustedClientArea(const QRect& desktop, const QRect& area) const; + + xcb_colormap_t colormap() const; + + /// Updates visibility depending on being shaded, virtual desktop, etc. + void updateVisibility(); + /// Hides a client - Basically like minimize, but without effects, it's simply hidden + void hideClient(bool hide) override; + bool hiddenPreview() const; ///< Window is mapped in order to get a window pixmap + + virtual bool setupCompositing(); + void finishCompositing(ReleaseReason releaseReason = ReleaseReason::Release) override; + void setBlockingCompositing(bool block); + inline bool isBlockingCompositing() { return blocks_compositing; } + + QString captionNormal() const override { + return cap_normal; + } + QString captionSuffix() const override { + return cap_suffix; + } + + using AbstractClient::keyPressEvent; + void keyPressEvent(uint key_code, xcb_timestamp_t time); // FRAME ?? + void updateMouseGrab() override; + xcb_window_t moveResizeGrabWindow() const; + + const QPoint calculateGravitation(bool invert, int gravity = 0) const; // FRAME public? + + void NETMoveResize(int x_root, int y_root, NET::Direction direction); + void NETMoveResizeWindow(int flags, int x, int y, int width, int height); + void restackWindow(xcb_window_t above, int detail, NET::RequestSource source, xcb_timestamp_t timestamp, + bool send_event = false); + + void gotPing(xcb_timestamp_t timestamp); + + void updateUserTime(xcb_timestamp_t time = XCB_TIME_CURRENT_TIME); + xcb_timestamp_t userTime() const override; + bool hasUserTimeSupport() const; + + /// Does 'delete c;' + static void deleteClient(Client* c); + + static bool belongToSameApplication(const Client* c1, const Client* c2, SameApplicationChecks checks = SameApplicationChecks()); + static bool sameAppWindowRoleMatch(const Client* c1, const Client* c2, bool active_hack); + + void killWindow() override; + void toggleShade(); + void showContextHelp() override; + void cancelShadeHoverTimer(); + void checkActiveModal(); + StrutRect strutRect(StrutArea area) const; + StrutRects strutRects() const; + bool hasStrut() const override; + + /* + * If shown is true the client is mapped and raised, if false + * the client is unmapped and hidden, this function is called + * when the tabbing group of the client switches its visible + * client. + */ + void setClientShown(bool shown) override; + + /** + * Whether or not the window has a strut that expands through the invisible area of + * an xinerama setup where the monitors are not the same resolution. + */ + bool hasOffscreenXineramaStrut() const; + + // Decorations <-> Effects + QRect decorationRect() const; + + QRect transparentRect() const; + + bool isClientSideDecorated() const; + bool wantsShadowToBeRendered() const override; + + void layoutDecorationRects(QRect &left, QRect &top, QRect &right, QRect &bottom) const override; + + Xcb::Property fetchFirstInTabBox() const; + void readFirstInTabBox(Xcb::Property &property); + void updateFirstInTabBox(); + Xcb::StringProperty fetchColorScheme() const; + void readColorScheme(Xcb::StringProperty &property); + void updateColorScheme() override; + + //sets whether the client should be faked as being on all activities (and be shown during session save) + void setSessionActivityOverride(bool needed); + virtual bool isClient() const; + + template + void print(T &stream) const; + + void cancelFocusOutTimer(); + + /** + * Restores the Client after it had been hidden due to show on screen edge functionality. + * In addition the property gets deleted so that the Client knows that it is visible again. + **/ + void showOnScreenEdge() override; + + Xcb::StringProperty fetchApplicationMenuServiceName() const; + void readApplicationMenuServiceName(Xcb::StringProperty &property); + void checkApplicationMenuServiceName(); + + Xcb::StringProperty fetchApplicationMenuObjectPath() const; + void readApplicationMenuObjectPath(Xcb::StringProperty &property); + void checkApplicationMenuObjectPath(); + + struct SyncRequest { + xcb_sync_counter_t counter; + xcb_sync_int64_t value; + xcb_sync_alarm_t alarm; + xcb_timestamp_t lastTimestamp; + QTimer *timeout, *failsafeTimeout; + bool isPending; + }; + const SyncRequest &getSyncRequest() const { + return syncRequest; + } + void handleSync(); + + static void cleanupX11(); + +public Q_SLOTS: + void closeWindow() override; + void updateCaption() override; + +private Q_SLOTS: + void shadeHover(); + void shadeUnhover(); + +private: + // Use Workspace::createClient() + virtual ~Client(); ///< Use destroyClient() or releaseWindow() + + // Handlers for X11 events + bool mapRequestEvent(xcb_map_request_event_t *e); + void unmapNotifyEvent(xcb_unmap_notify_event_t *e); + void destroyNotifyEvent(xcb_destroy_notify_event_t *e); + void configureRequestEvent(xcb_configure_request_event_t *e); + virtual void propertyNotifyEvent(xcb_property_notify_event_t *e) override; + void clientMessageEvent(xcb_client_message_event_t *e) override; + void enterNotifyEvent(xcb_enter_notify_event_t *e); + void leaveNotifyEvent(xcb_leave_notify_event_t *e); + void focusInEvent(xcb_focus_in_event_t *e); + void focusOutEvent(xcb_focus_out_event_t *e); + virtual void damageNotifyEvent(); + + bool buttonPressEvent(xcb_window_t w, int button, int state, int x, int y, int x_root, int y_root, xcb_timestamp_t time = XCB_CURRENT_TIME); + bool buttonReleaseEvent(xcb_window_t w, int button, int state, int x, int y, int x_root, int y_root); + bool motionNotifyEvent(xcb_window_t w, int state, int x, int y, int x_root, int y_root); + + Client* findAutogroupCandidate() const; + +protected: + virtual void debug(QDebug& stream) const; + void addDamage(const QRegion &damage) override; + bool belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const override; + void doSetActive() override; + void doSetKeepAbove() override; + void doSetKeepBelow() override; + void doSetDesktop(int desktop, int was_desk) override; + void doMinimize() override; + void doSetSkipPager() override; + void doSetSkipTaskbar() override; + void doSetSkipSwitcher() override; + bool belongsToDesktop() const override; + void setGeometryRestore(const QRect &geo) override; + void updateTabGroupStates(TabGroup::States states) override; + void doMove(int x, int y) override; + bool doStartMoveResize() override; + void doPerformMoveResize() override; + bool isWaitingForMoveResizeSync() const override; + void doResizeSync() override; + QSize resizeIncrements() const override; + bool acceptsFocus() const override; + + //Signals for the scripting interface + //Signals make an excellent way for communication + //in between objects as compared to simple function + //calls +Q_SIGNALS: + void clientManaging(KWin::Client*); + void clientFullScreenSet(KWin::Client*, bool, bool); + + /** + * Emitted whenever the Client want to show it menu + */ + void showRequest(); + /** + * Emitted whenever the Client's menu is closed + */ + void menuHidden(); + /** + * Emitted whenever the Client's menu is available + **/ + void appMenuAvailable(); + /** + * Emitted whenever the Client's menu is unavailable + */ + void appMenuUnavailable(); + + /** + * Emitted whenever the Client's block compositing state changes. + **/ + void blockingCompositingChanged(KWin::Client *client); + void clientSideDecoratedChanged(); + +private: + void exportMappingState(int s); // ICCCM 4.1.3.1, 4.1.4, NETWM 2.5.1 + bool isManaged() const; ///< Returns false if this client is not yet managed + void updateAllowedActions(bool force = false); + QRect fullscreenMonitorsArea(NETFullscreenMonitors topology) const; + void changeMaximize(bool horizontal, bool vertical, bool adjust) override; + int checkFullScreenHack(const QRect& geom) const; // 0 - None, 1 - One xinerama screen, 2 - Full area + void updateFullScreenHack(const QRect& geom); + void getWmNormalHints(); + void getMotifHints(); + void getIcons(); + void fetchName(); + void fetchIconicName(); + QString readName() const; + void setCaption(const QString& s, bool force = false); + bool hasTransientInternal(const Client* c, bool indirect, ConstClientList& set) const; + void setShortcutInternal() override; + + void configureRequest(int value_mask, int rx, int ry, int rw, int rh, int gravity, bool from_tool); + NETExtendedStrut strut() const; + int checkShadeGeometry(int w, int h); + void getSyncCounter(); + void sendSyncRequest(); + void leaveMoveResize() override; + void positionGeometryTip() override; + void grabButton(int mod); + void ungrabButton(int mod); + void resizeDecoration(); + void createDecoration(const QRect &oldgeom); + + void pingWindow(); + void killProcess(bool ask, xcb_timestamp_t timestamp = XCB_TIME_CURRENT_TIME); + void updateUrgency(); + static void sendClientMessage(xcb_window_t w, xcb_atom_t a, xcb_atom_t protocol, + uint32_t data1 = 0, uint32_t data2 = 0, uint32_t data3 = 0, + xcb_timestamp_t timestamp = xTime()); + + void embedClient(xcb_window_t w, xcb_visualid_t visualid, xcb_colormap_t colormap, uint8_t depth); + void detectNoBorder(); + Xcb::Property fetchGtkFrameExtents() const; + void readGtkFrameExtents(Xcb::Property &prop); + void detectGtkFrameExtents(); + void destroyDecoration() override; + void updateFrameExtents(); + + void internalShow(); + void internalHide(); + void internalKeep(); + void map(); + void unmap(); + void updateHiddenPreview(); + + void updateInputShape(); + + xcb_timestamp_t readUserTimeMapTimestamp(const KStartupInfoId* asn_id, const KStartupInfoData* asn_data, + bool session) const; + xcb_timestamp_t readUserCreationTime() const; + void startupIdChanged(); + + void updateInputWindow(); + + Xcb::Property fetchShowOnScreenEdge() const; + void readShowOnScreenEdge(Xcb::Property &property); + /** + * Reads the property and creates/destroys the screen edge if required + * and shows/hides the client. + **/ + void updateShowOnScreenEdge(); + + Xcb::Window m_client; + Xcb::Window m_wrapper; + Xcb::Window m_frame; + QStringList activityList; + int m_activityUpdatesBlocked; + bool m_blockedActivityUpdatesRequireTransients; + Xcb::Window m_moveResizeGrabWindow; + bool move_resize_has_keyboard_grab; + bool m_managed; + + Xcb::GeometryHints m_geometryHints; + void sendSyntheticConfigureNotify(); + enum MappingState { + Withdrawn, ///< Not handled, as per ICCCM WithdrawnState + Mapped, ///< The frame is mapped + Unmapped, ///< The frame is not mapped + Kept ///< The frame should be unmapped, but is kept (For compositing) + }; + MappingState mapping_state; + + Xcb::TransientFor fetchTransient() const; + void readTransientProperty(Xcb::TransientFor &transientFor); + void readTransient(); + xcb_window_t verifyTransientFor(xcb_window_t transient_for, bool set); + void addTransient(AbstractClient* cl) override; + void removeTransient(AbstractClient* cl) override; + void removeFromMainClients(); + void cleanGrouping(); + void checkGroupTransients(); + void setTransient(xcb_window_t new_transient_for_id); + xcb_window_t m_transientForId; + xcb_window_t m_originalTransientForId; + ShadeMode shade_mode; + Client *shade_below; + uint deleting : 1; ///< True when doing cleanup and destroying the client + Xcb::MotifHints m_motif; + uint hidden : 1; ///< Forcibly hidden by calling hide() + uint noborder : 1; + uint app_noborder : 1; ///< App requested no border via window type, shape extension, etc. + uint ignore_focus_stealing : 1; ///< Don't apply focus stealing prevention to this client + bool blocks_compositing; + // DON'T reorder - Saved to config files !!! + enum FullScreenMode { + FullScreenNone, + FullScreenNormal, + FullScreenHack ///< Non-NETWM fullscreen (noborder and size of desktop) + }; + FullScreenMode fullscreen_mode; + MaximizeMode max_mode; + QRect geom_restore; + QRect geom_fs_restore; + QTimer* shadeHoverTimer; + xcb_colormap_t m_colormap; + QString cap_normal, cap_iconic, cap_suffix; + Group* in_group; + QTimer* ping_timer; + qint64 m_killHelperPID; + xcb_timestamp_t m_pingTimestamp; + xcb_timestamp_t m_userTime; + NET::Actions allowed_actions; + QSize client_size; + bool shade_geometry_change; + SyncRequest syncRequest; + static bool check_active_modal; ///< \see Client::checkActiveModal() + int sm_stacking_order; + friend struct ResetupRulesProcedure; + + friend bool performTransiencyCheck(); + + Xcb::StringProperty fetchActivities() const; + void readActivities(Xcb::StringProperty &property); + void checkActivities(); + bool activitiesDefined; //whether the x property was actually set + + bool sessionActivityOverride; + bool needsXWindowMove; + + Xcb::Window m_decoInputExtent; + QPoint input_offset; + + QTimer *m_focusOutTimer; + + QList m_connections; + bool m_clientSideDecorated; + + QMetaObject::Connection m_edgeRemoveConnection; + QMetaObject::Connection m_edgeGeometryTrackingConnection; +}; + +inline xcb_window_t Client::wrapperId() const +{ + return m_wrapper; +} + +inline bool Client::isClientSideDecorated() const +{ + return m_clientSideDecorated; +} + +inline bool Client::groupTransient() const +{ + return m_transientForId == rootWindow(); +} + +// Needed because verifyTransientFor() may set transient_for_id to root window, +// if the original value has a problem (window doesn't exist, etc.) +inline bool Client::wasOriginallyGroupTransient() const +{ + return m_originalTransientForId == rootWindow(); +} + +inline bool Client::isTransient() const +{ + return m_transientForId != XCB_WINDOW_NONE; +} + +inline const Group* Client::group() const +{ + return in_group; +} + +inline Group* Client::group() +{ + return in_group; +} + +inline bool Client::isShown(bool shaded_is_shown) const +{ + return !isMinimized() && (!isShade() || shaded_is_shown) && !hidden && + (!tabGroup() || tabGroup()->current() == this); +} + +inline bool Client::isHiddenInternal() const +{ + return hidden; +} + +inline ShadeMode Client::shadeMode() const +{ + return shade_mode; +} + +inline QRect Client::geometryRestore() const +{ + return geom_restore; +} + +inline void Client::setGeometryRestore(const QRect &geo) +{ + geom_restore = geo; +} + +inline MaximizeMode Client::maximizeMode() const +{ + return max_mode; +} + +inline bool Client::isFullScreen() const +{ + return fullscreen_mode != FullScreenNone; +} + +inline bool Client::hasNETSupport() const +{ + return info->hasNETSupport(); +} + +inline xcb_colormap_t Client::colormap() const +{ + return m_colormap; +} + +inline int Client::sessionStackingOrder() const +{ + return sm_stacking_order; +} + +inline bool Client::isManaged() const +{ + return m_managed; +} + +inline QSize Client::clientSize() const +{ + return client_size; +} + +inline void Client::plainResize(const QSize& s, ForceGeometry_t force) +{ + plainResize(s.width(), s.height(), force); +} + +inline void Client::resizeWithChecks(int w, int h, AbstractClient::ForceGeometry_t force) +{ + resizeWithChecks(w, h, XCB_GRAVITY_BIT_FORGET, force); +} + +inline void Client::resizeWithChecks(const QSize& s, xcb_gravity_t gravity, ForceGeometry_t force) +{ + resizeWithChecks(s.width(), s.height(), gravity, force); +} + +inline bool Client::hasUserTimeSupport() const +{ + return info->userTime() != -1U; +} + +inline xcb_window_t Client::moveResizeGrabWindow() const +{ + return m_moveResizeGrabWindow; +} + +inline bool Client::hiddenPreview() const +{ + return mapping_state == Kept; +} + +template +inline void Client::print(T &stream) const +{ + stream << "\'ID:" << window() << ";WMCLASS:" << resourceClass() << ":" + << resourceName() << ";Caption:" << caption() << "\'"; +} + +} // namespace +Q_DECLARE_METATYPE(KWin::Client*) +Q_DECLARE_METATYPE(QList) + +#endif diff --git a/client_machine.cpp b/client_machine.cpp new file mode 100644 index 0000000..764b137 --- /dev/null +++ b/client_machine.cpp @@ -0,0 +1,242 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +// 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(NULL) + , m_ownAddress(NULL) + , 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..e56ffc7 --- /dev/null +++ b/client_machine.h @@ -0,0 +1,117 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 = NULL); + virtual ~GetAddrInfo(); + + 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 = NULL); + virtual ~ClientMachine(); + + 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..d95e46b --- /dev/null +++ b/cmake/modules/FindFontconfig.cmake @@ -0,0 +1,50 @@ +# - Try to find the Fontconfig +# Once done this will define +# +# FONTCONFIG_FOUND - system has Fontconfig +# FONTCONFIG_INCLUDE_DIR - The include directory to use for the fontconfig headers +# FONTCONFIG_LIBRARIES - Link these to use FONTCONFIG +# FONTCONFIG_DEFINITIONS - Compiler switches required for using FONTCONFIG + +# Copyright (c) 2006,2007 Laurent Montel, +# +# Redistribution and use is allowed according to the terms of the BSD license. +# For details see the accompanying COPYING-CMAKE-SCRIPTS file. + + +if (FONTCONFIG_LIBRARIES AND FONTCONFIG_INCLUDE_DIR) + + # in cache already + set(FONTCONFIG_FOUND TRUE) + +else (FONTCONFIG_LIBRARIES AND FONTCONFIG_INCLUDE_DIR) + + 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(PC_FONTCONFIG QUIET fontconfig) + + set(FONTCONFIG_DEFINITIONS ${PC_FONTCONFIG_CFLAGS_OTHER}) + endif (NOT WIN32) + + find_path(FONTCONFIG_INCLUDE_DIR fontconfig/fontconfig.h + PATHS + ${PC_FONTCONFIG_INCLUDEDIR} + ${PC_FONTCONFIG_INCLUDE_DIRS} + /usr/X11/include + ) + + find_library(FONTCONFIG_LIBRARIES NAMES fontconfig + PATHS + ${PC_FONTCONFIG_LIBDIR} + ${PC_FONTCONFIG_LIBRARY_DIRS} + ) + + include(FindPackageHandleStandardArgs) + find_package_handle_standard_args(Fontconfig DEFAULT_MSG FONTCONFIG_LIBRARIES FONTCONFIG_INCLUDE_DIR ) + + mark_as_advanced(FONTCONFIG_LIBRARIES FONTCONFIG_INCLUDE_DIR) + +endif (FONTCONFIG_LIBRARIES AND FONTCONFIG_INCLUDE_DIR) + diff --git a/cmake/modules/FindLibcap.cmake b/cmake/modules/FindLibcap.cmake new file mode 100644 index 0000000..4a32446 --- /dev/null +++ b/cmake/modules/FindLibcap.cmake @@ -0,0 +1,59 @@ +# 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 +# + + +# Copyright (c) 2014, Hrvoje Senjan, +# +# 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. + +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..9936e07 --- /dev/null +++ b/cmake/modules/FindLibdrm.cmake @@ -0,0 +1,126 @@ +#.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. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# +# 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. +#============================================================================= + +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..b856e0b --- /dev/null +++ b/cmake/modules/FindLibinput.cmake @@ -0,0 +1,125 @@ +#.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. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# +# 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. +#============================================================================= + +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 "http://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/FindQt5EventDispatcherSupport.cmake b/cmake/modules/FindQt5EventDispatcherSupport.cmake new file mode 100644 index 0000000..948efe0 --- /dev/null +++ b/cmake/modules/FindQt5EventDispatcherSupport.cmake @@ -0,0 +1,122 @@ +#.rst: +# FindQt5EventDispatcherSupport +# ------- +# +# Try to find Qt5EventDispatcherSupport on a Unix system. +# +# This will define the following variables: +# +# ``Qt5EventDispatcherSupport_FOUND`` +# True if (the requested version of) Qt5EventDispatcherSupport is available +# ``Qt5EventDispatcherSupport_VERSION`` +# The version of Qt5EventDispatcherSupport +# ``Qt5EventDispatcherSupport_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``Qt5EventDispatcherSupport::Qt5EventDispatcherSupport`` +# target +# ``Qt5EventDispatcherSupport_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``Qt5EventDispatcherSupport_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``Qt5EventDispatcherSupport_FOUND`` is TRUE, it will also define the following imported target: +# +# ``Qt5EventDispatcherSupport::Qt5EventDispatcherSupport`` +# The Qt5EventDispatcherSupport 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. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# Copyright 2016 Takahiro Hashimoto +# +# 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. +#============================================================================= + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "CMake 2.8.12 is required by FindQt5EventDispatcherSupport.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 FindQt5EventDispatcherSupport.cmake") +endif() + +# 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_Qt5EventDispatcherSupport QUIET Qt5Gui) + +set(Qt5EventDispatcherSupport_DEFINITIONS ${PKG_Qt5EventDispatcherSupport_CFLAGS_OTHER}) +set(Qt5EventDispatcherSupport_VERSION ${PKG_Qt5EventDispatcherSupport_VERSION}) + +find_path(Qt5EventDispatcherSupport_INCLUDE_DIR + NAMES + QtEventDispatcherSupport/private/qunixeventdispatcher_qpa_p.h + HINTS + ${PKG_Qt5EventDispatcherSupport_INCLUDEDIR}/QtEventDispatcherSupport/${PKG_Qt5EventDispatcherSupport_VERSION}/ +) +find_library(Qt5EventDispatcherSupport_LIBRARY + NAMES + Qt5EventDispatcherSupport + HINTS + ${PKG_Qt5EventDispatcherSupport_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Qt5EventDispatcherSupport + FOUND_VAR + Qt5EventDispatcherSupport_FOUND + REQUIRED_VARS + Qt5EventDispatcherSupport_LIBRARY + Qt5EventDispatcherSupport_INCLUDE_DIR + VERSION_VAR + Qt5EventDispatcherSupport_VERSION +) + +if(Qt5EventDispatcherSupport_FOUND AND NOT TARGET Qt5EventDispatcherSupport::Qt5EventDispatcherSupport) + add_library(Qt5EventDispatcherSupport::Qt5EventDispatcherSupport UNKNOWN IMPORTED) + set_target_properties(Qt5EventDispatcherSupport::Qt5EventDispatcherSupport PROPERTIES + IMPORTED_LOCATION "${Qt5EventDispatcherSupport_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${Qt5EventDispatcherSupport_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${Qt5EventDispatcherSupport_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(Qt5EventDispatcherSupport_LIBRARY Qt5EventDispatcherSupport_INCLUDE_DIR) + +# compatibility variables +set(Qt5EventDispatcherSupport_LIBRARIES ${Qt5EventDispatcherSupport_LIBRARY}) +set(Qt5EventDispatcherSupport_INCLUDE_DIRS ${Qt5EventDispatcherSupport_INCLUDE_DIR}) +set(Qt5EventDispatcherSupport_VERSION_STRING ${Qt5EventDispatcherSupport_VERSION}) + + +include(FeatureSummary) +set_package_properties(Qt5EventDispatcherSupport PROPERTIES + URL "http://www.qt.io" + DESCRIPTION "Qt EventDispatcherSupport module." +) + diff --git a/cmake/modules/FindQt5FontDatabaseSupport.cmake b/cmake/modules/FindQt5FontDatabaseSupport.cmake new file mode 100644 index 0000000..d3e66cd --- /dev/null +++ b/cmake/modules/FindQt5FontDatabaseSupport.cmake @@ -0,0 +1,122 @@ +#.rst: +# FindQt5FontDatabaseSupport +# ------- +# +# Try to find Qt5FontDatabaseSupport on a Unix system. +# +# This will define the following variables: +# +# ``Qt5FontDatabaseSupport_FOUND`` +# True if (the requested version of) Qt5FontDatabaseSupport is available +# ``Qt5FontDatabaseSupport_VERSION`` +# The version of Qt5FontDatabaseSupport +# ``Qt5FontDatabaseSupport_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``Qt5FontDatabaseSupport::Qt5FontDatabaseSupport`` +# target +# ``Qt5FontDatabaseSupport_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``Qt5FontDatabaseSupport_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``Qt5FontDatabaseSupport_FOUND`` is TRUE, it will also define the following imported target: +# +# ``Qt5FontDatabaseSupport::Qt5FontDatabaseSupport`` +# The Qt5FontDatabaseSupport 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. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# Copyright 2016 Takahiro Hashimoto +# +# 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. +#============================================================================= + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "CMake 2.8.12 is required by FindQt5FontDatabaseSupport.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 FindQt5FontDatabaseSupport.cmake") +endif() + +# 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_Qt5FontDatabaseSupport QUIET Qt5Gui) + +set(Qt5FontDatabaseSupport_DEFINITIONS ${PKG_Qt5FontDatabaseSupport_CFLAGS_OTHER}) +set(Qt5FontDatabaseSupport_VERSION ${PKG_Qt5FontDatabaseSupport_VERSION}) + +find_path(Qt5FontDatabaseSupport_INCLUDE_DIR + NAMES + QtFontDatabaseSupport/private/qfontconfigdatabase_p.h + HINTS + ${PKG_Qt5FontDatabaseSupport_INCLUDEDIR}/QtFontDatabaseSupport/${PKG_Qt5FontDatabaseSupport_VERSION}/ +) +find_library(Qt5FontDatabaseSupport_LIBRARY + NAMES + Qt5FontDatabaseSupport + HINTS + ${PKG_Qt5FontDatabaseSupport_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Qt5FontDatabaseSupport + FOUND_VAR + Qt5FontDatabaseSupport_FOUND + REQUIRED_VARS + Qt5FontDatabaseSupport_LIBRARY + Qt5FontDatabaseSupport_INCLUDE_DIR + VERSION_VAR + Qt5FontDatabaseSupport_VERSION +) + +if(Qt5FontDatabaseSupport_FOUND AND NOT TARGET Qt5FontDatabaseSupport::Qt5FontDatabaseSupport) + add_library(Qt5FontDatabaseSupport::Qt5FontDatabaseSupport UNKNOWN IMPORTED) + set_target_properties(Qt5FontDatabaseSupport::Qt5FontDatabaseSupport PROPERTIES + IMPORTED_LOCATION "${Qt5FontDatabaseSupport_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${Qt5FontDatabaseSupport_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${Qt5FontDatabaseSupport_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(Qt5FontDatabaseSupport_LIBRARY Qt5FontDatabaseSupport_INCLUDE_DIR) + +# compatibility variables +set(Qt5FontDatabaseSupport_LIBRARIES ${Qt5FontDatabaseSupport_LIBRARY}) +set(Qt5FontDatabaseSupport_INCLUDE_DIRS ${Qt5FontDatabaseSupport_INCLUDE_DIR}) +set(Qt5FontDatabaseSupport_VERSION_STRING ${Qt5FontDatabaseSupport_VERSION}) + + +include(FeatureSummary) +set_package_properties(Qt5FontDatabaseSupport PROPERTIES + URL "http://www.qt.io" + DESCRIPTION "Qt FontDatabaseSupport module." +) + diff --git a/cmake/modules/FindQt5PlatformSupport.cmake b/cmake/modules/FindQt5PlatformSupport.cmake new file mode 100644 index 0000000..da6a1c1 --- /dev/null +++ b/cmake/modules/FindQt5PlatformSupport.cmake @@ -0,0 +1,121 @@ +#.rst: +# FindQt5PlatformSupport +# ------- +# +# Try to find Qt5PlatformSupport on a Unix system. +# +# This will define the following variables: +# +# ``Qt5PlatformSupport_FOUND`` +# True if (the requested version of) Qt5PlatformSupport is available +# ``Qt5PlatformSupport_VERSION`` +# The version of Qt5PlatformSupport +# ``Qt5PlatformSupport_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``Qt5PlatformSupport::Qt5PlatformSupport`` +# target +# ``Qt5PlatformSupport_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``Qt5PlatformSupport_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``Qt5PlatformSupport_FOUND`` is TRUE, it will also define the following imported target: +# +# ``Qt5PlatformSupport::Qt5PlatformSupport`` +# The Qt5PlatformSupport 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. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# +# 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. +#============================================================================= + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "CMake 2.8.12 is required by FindQt5PlatformSupport.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 FindQt5PlatformSupport.cmake") +endif() + +# 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_Qt5PlatformSupport QUIET Qt5Gui) + +set(Qt5PlatformSupport_DEFINITIONS ${PKG_Qt5PlatformSupport_CFLAGS_OTHER}) +set(Qt5PlatformSupport_VERSION ${PKG_Qt5PlatformSupport_VERSION}) + +find_path(Qt5PlatformSupport_INCLUDE_DIR + NAMES + QtPlatformSupport/private/qfontconfigdatabase_p.h + HINTS + ${PKG_Qt5PlatformSupport_INCLUDEDIR}/QtPlatformSupport/${PKG_Qt5PlatformSupport_VERSION}/ +) +find_library(Qt5PlatformSupport_LIBRARY + NAMES + Qt5PlatformSupport + HINTS + ${PKG_Qt5PlatformSupport_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Qt5PlatformSupport + FOUND_VAR + Qt5PlatformSupport_FOUND + REQUIRED_VARS + Qt5PlatformSupport_LIBRARY + Qt5PlatformSupport_INCLUDE_DIR + VERSION_VAR + Qt5PlatformSupport_VERSION +) + +if(Qt5PlatformSupport_FOUND AND NOT TARGET Qt5PlatformSupport::Qt5PlatformSupport) + add_library(Qt5PlatformSupport::Qt5PlatformSupport UNKNOWN IMPORTED) + set_target_properties(Qt5PlatformSupport::Qt5PlatformSupport PROPERTIES + IMPORTED_LOCATION "${Qt5PlatformSupport_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${Qt5PlatformSupport_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${Qt5PlatformSupport_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(Qt5PlatformSupport_LIBRARY Qt5PlatformSupport_INCLUDE_DIR) + +# compatibility variables +set(Qt5PlatformSupport_LIBRARIES ${Qt5PlatformSupport_LIBRARY}) +set(Qt5PlatformSupport_INCLUDE_DIRS ${Qt5PlatformSupport_INCLUDE_DIR}) +set(Qt5PlatformSupport_VERSION_STRING ${Qt5PlatformSupport_VERSION}) + + +include(FeatureSummary) +set_package_properties(Qt5PlatformSupport PROPERTIES + URL "http://www.qt.io" + DESCRIPTION "Qt PlatformSupport module." +) + diff --git a/cmake/modules/FindQt5ThemeSupport.cmake b/cmake/modules/FindQt5ThemeSupport.cmake new file mode 100644 index 0000000..5588a76 --- /dev/null +++ b/cmake/modules/FindQt5ThemeSupport.cmake @@ -0,0 +1,122 @@ +#.rst: +# FindQt5ThemeSupport +# ------- +# +# Try to find Qt5ThemeSupport on a Unix system. +# +# This will define the following variables: +# +# ``Qt5ThemeSupport_FOUND`` +# True if (the requested version of) Qt5ThemeSupport is available +# ``Qt5ThemeSupport_VERSION`` +# The version of Qt5ThemeSupport +# ``Qt5ThemeSupport_LIBRARIES`` +# This can be passed to target_link_libraries() instead of the ``Qt5ThemeSupport::Qt5ThemeSupport`` +# target +# ``Qt5ThemeSupport_INCLUDE_DIRS`` +# This should be passed to target_include_directories() if the target is not +# used for linking +# ``Qt5ThemeSupport_DEFINITIONS`` +# This should be passed to target_compile_options() if the target is not +# used for linking +# +# If ``Qt5ThemeSupport_FOUND`` is TRUE, it will also define the following imported target: +# +# ``Qt5ThemeSupport::Qt5ThemeSupport`` +# The Qt5ThemeSupport 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. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# Copyright 2016 Takahiro Hashimoto +# +# 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. +#============================================================================= + +if(CMAKE_VERSION VERSION_LESS 2.8.12) + message(FATAL_ERROR "CMake 2.8.12 is required by FindQt5ThemeSupport.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 FindQt5ThemeSupport.cmake") +endif() + +# 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_Qt5ThemeSupport QUIET Qt5Gui) + +set(Qt5ThemeSupport_DEFINITIONS ${PKG_Qt5ThemeSupport_CFLAGS_OTHER}) +set(Qt5ThemeSupport_VERSION ${PKG_Qt5ThemeSupport_VERSION}) + +find_path(Qt5ThemeSupport_INCLUDE_DIR + NAMES + QtThemeSupport/private/qgenericunixthemes_p.h + HINTS + ${PKG_Qt5ThemeSupport_INCLUDEDIR}/QtThemeSupport/${PKG_Qt5ThemeSupport_VERSION}/ +) +find_library(Qt5ThemeSupport_LIBRARY + NAMES + Qt5ThemeSupport + HINTS + ${PKG_Qt5ThemeSupport_LIBRARY_DIRS} +) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Qt5ThemeSupport + FOUND_VAR + Qt5ThemeSupport_FOUND + REQUIRED_VARS + Qt5ThemeSupport_LIBRARY + Qt5ThemeSupport_INCLUDE_DIR + VERSION_VAR + Qt5ThemeSupport_VERSION +) + +if(Qt5ThemeSupport_FOUND AND NOT TARGET Qt5ThemeSupport::Qt5ThemeSupport) + add_library(Qt5ThemeSupport::Qt5ThemeSupport UNKNOWN IMPORTED) + set_target_properties(Qt5ThemeSupport::Qt5ThemeSupport PROPERTIES + IMPORTED_LOCATION "${Qt5ThemeSupport_LIBRARY}" + INTERFACE_COMPILE_OPTIONS "${Qt5ThemeSupport_DEFINITIONS}" + INTERFACE_INCLUDE_DIRECTORIES "${Qt5ThemeSupport_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(Qt5ThemeSupport_LIBRARY Qt5ThemeSupport_INCLUDE_DIR) + +# compatibility variables +set(Qt5ThemeSupport_LIBRARIES ${Qt5ThemeSupport_LIBRARY}) +set(Qt5ThemeSupport_INCLUDE_DIRS ${Qt5ThemeSupport_INCLUDE_DIR}) +set(Qt5ThemeSupport_VERSION_STRING ${Qt5ThemeSupport_VERSION}) + + +include(FeatureSummary) +set_package_properties(Qt5ThemeSupport PROPERTIES + URL "http://www.qt.io" + DESCRIPTION "Qt ThemeSupport module." +) + diff --git a/cmake/modules/FindUDev.cmake b/cmake/modules/FindUDev.cmake new file mode 100644 index 0000000..9d0f21d --- /dev/null +++ b/cmake/modules/FindUDev.cmake @@ -0,0 +1,50 @@ +# - 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 + +# Copyright (c) 2010, Rafael Fernández López, +# +# 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 University 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 REGENTS 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 REGENTS 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. + +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..0d599df --- /dev/null +++ b/cmake/modules/FindXKB.cmake @@ -0,0 +1,101 @@ +# Try to find xkbcommon on a Unix system +# +# This will define: +# +# 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 +# +# Copyright (c) 2014 Martin Gräßlin +# +# 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 University 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 REGENTS 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 REGENTS 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. + +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 "http://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..1f52be5 --- /dev/null +++ b/cmake/modules/FindXwayland.cmake @@ -0,0 +1,34 @@ +#============================================================================= +# Copyright 2016 Martin Gräßlin +# +# 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. +#============================================================================= +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/Findepoxy.cmake b/cmake/modules/Findepoxy.cmake new file mode 100644 index 0000000..dfd8c3c --- /dev/null +++ b/cmake/modules/Findepoxy.cmake @@ -0,0 +1,56 @@ +# - 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 + +# Copyright (c) 2014 Fredrik Höglund +# +# 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 University 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 REGENTS 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 REGENTS 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. + +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..6dfc895 --- /dev/null +++ b/cmake/modules/Findgbm.cmake @@ -0,0 +1,125 @@ +#.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. + +#============================================================================= +# Copyright 2014 Alex Merry +# Copyright 2014 Martin Gräßlin +# +# 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. +#============================================================================= + +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 "http://www.mesa3d.org" + DESCRIPTION "Mesa gbm library." +) diff --git a/cmake/modules/Findlibhybris.cmake b/cmake/modules/Findlibhybris.cmake new file mode 100644 index 0000000..a763fd8 --- /dev/null +++ b/cmake/modules/Findlibhybris.cmake @@ -0,0 +1,185 @@ +#.rst: +# Findlibhybris +# ------- +# +# Try to find libhybris on a Unix system. + +#============================================================================= +# Copyright 2015 Martin Gräßlin +# +# 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. +#============================================================================= + +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/colorcorrect_settings.kcfg b/colorcorrection/colorcorrect_settings.kcfg new file mode 100644 index 0000000..616d062 --- /dev/null +++ b/colorcorrection/colorcorrect_settings.kcfg @@ -0,0 +1,56 @@ + + + + + + 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..5376d9b --- /dev/null +++ b/colorcorrection/colorcorrectdbusinterface.cpp @@ -0,0 +1,54 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2017 Roman Gilg + +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, see . +*********************************************************************/ + +#include "colorcorrectdbusinterface.h" +#include "colorcorrectadaptor.h" + +#include "manager.h" + +namespace KWin { +namespace ColorCorrect { + +ColorCorrectDBusInterface::ColorCorrectDBusInterface(Manager *parent) + : QObject(parent) + , m_manager(parent) +{ + connect(m_manager, &Manager::configChange, this, &ColorCorrectDBusInterface::nightColorConfigChanged); + new ColorCorrectAdaptor(this); + QDBusConnection::sessionBus().registerObject(QStringLiteral("/ColorCorrect"), this); +} + +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); +} + +} +} diff --git a/colorcorrection/colorcorrectdbusinterface.h b/colorcorrection/colorcorrectdbusinterface.h new file mode 100644 index 0000000..965b017 --- /dev/null +++ b/colorcorrection/colorcorrectdbusinterface.h @@ -0,0 +1,126 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2017 Roman Gilg + +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, see . +*********************************************************************/ + +#ifndef KWIN_NIGHTCOLOR_DBUS_INTERFACE_H +#define KWIN_NIGHTCOLOR_DBUS_INTERFACE_H + +#include +#include + +namespace KWin +{ + +namespace ColorCorrect +{ + +class Manager; + +class ColorCorrectDBusInterface : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.ColorCorrect") + +public: + explicit ColorCorrectDBusInterface(Manager *parent); + virtual ~ColorCorrectDBusInterface() = default; + +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); + +Q_SIGNALS: + /** + * @brief Emits that the Night Color configuration has been changed. + * + * The provided variant hash provides the same fields as @link nightColorInfo + * + * @return void + * @see nightColorInfo + * @see nightColorConfigChange + * @since 5.12 + **/ + void nightColorConfigChanged(QHash data); + +private: + Manager *m_manager; +}; + +} + +} + +#endif // KWIN_NIGHTCOLOR_DBUS_INTERFACE_H diff --git a/colorcorrection/constants.h b/colorcorrection/constants.h new file mode 100644 index 0000000..868079c --- /dev/null +++ b/colorcorrection/constants.h @@ -0,0 +1,288 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2017 Roman Gilg + +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, see . +*********************************************************************/ +#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/gammaramp.h b/colorcorrection/gammaramp.h new file mode 100644 index 0000000..53ef606 --- /dev/null +++ b/colorcorrection/gammaramp.h @@ -0,0 +1,50 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2017 Roman Gilg + +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, see . +*********************************************************************/ +#ifndef KWIN_GAMMARAMP_H +#define KWIN_GAMMARAMP_H + +namespace KWin +{ + +namespace ColorCorrect +{ + +struct GammaRamp { + GammaRamp(int _size) { + size = _size; + red = new uint16_t[3 * _size]; + green = red + _size; + blue = green + _size; + } + ~GammaRamp() { + delete[] red; + red = green = blue = nullptr; + } + + uint32_t size = 0; + uint16_t *red = nullptr; + uint16_t *green = nullptr; + uint16_t *blue = nullptr; +}; + +} +} + +#endif // KWIN_GAMMARAMP_H diff --git a/colorcorrection/manager.cpp b/colorcorrection/manager.cpp new file mode 100644 index 0000000..0cd0369 --- /dev/null +++ b/colorcorrection/manager.cpp @@ -0,0 +1,779 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2017 Roman Gilg + +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, see . +*********************************************************************/ +#include "manager.h" +#include "colorcorrectdbusinterface.h" +#include "suncalc.h" +#include "gammaramp.h" +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#ifdef Q_OS_LINUX +#include +#endif +#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); + connect(kwinApp(), &Application::workspaceCreated, this, &Manager::init); +} + +void Manager::init() +{ + Settings::instance(kwinApp()->config()); + // we may always read in the current config + readConfig(); + + if (!kwinApp()->platform()->supportsGammaControl()) { + // at least update the sun timings to make the values accessible via dbus + updateSunTimings(true); + return; + } + + connect(Screens::self(), &Screens::countChanged, this, &Manager::hardReset); + + connect(LogindIntegration::self(), &LogindIntegration::sessionActiveChanged, this, + [this](bool active) { + if (active) { + hardReset(); + } else { + cancelAllTimers(); + } + } + ); + +#ifdef Q_OS_LINUX + // monitor for system clock changes - from the time dataengine + auto timeChangedFd = ::timerfd_create(CLOCK_REALTIME, O_CLOEXEC | O_NONBLOCK); + ::itimerspec timespec; + //set all timers to 0, which creates a timer that won't do anything + ::memset(×pec, 0, sizeof(timespec)); + + // Monitor for the time changing (flags == TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET). + // However these are not exposed in glibc so value is hardcoded: + ::timerfd_settime(timeChangedFd, 3, ×pec, 0); + + connect(this, &QObject::destroyed, [timeChangedFd]() { + ::close(timeChangedFd); + }); + + auto notifier = new QSocketNotifier(timeChangedFd, QSocketNotifier::Read, this); + connect(notifier, &QSocketNotifier::activated, this, [this](int fd) { + uint64_t c; + ::read(fd, &c, 8); + + // 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(); + } + }); +#else + // TODO: Alternative method for BSD. +#endif + + hardReset(); +} + +void Manager::hardReset() +{ + cancelAllTimers(); + updateSunTimings(true); + if (kwinApp()->platform()->supportsGammaControl() && m_active) { + m_running = true; + commitGammaRamps(currentTargetTemp()); + } + resetAllTimers(); +} + +void Manager::reparseConfigAndReset() +{ + cancelAllTimers(); + readConfig(); + hardReset(); +} + +void Manager::readConfig() +{ + Settings *s = Settings::self(); + s->load(); + + m_active = s->active(); + + NightColorMode mode = s->mode(); + if (mode == NightColorMode::Location || mode == NightColorMode::Timings) { + m_mode = mode; + } else { + // also fallback for invalid setting values + m_mode = NightColorMode::Automatic; + } + + 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 (kwinApp()->platform()->supportsGammaControl()) { + if (m_active) { + m_running = true; + } + // we do this also for active being false in order to reset the temperature back to the day value + resetQuickAdjustTimer(); + } else { + m_running = 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() +{ + updateSunTimings(false); + + 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; + 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; + } + + // set up the next slow update + m_slowUpdateStartTimer = new QTimer(this); + m_slowUpdateStartTimer->setSingleShot(true); + connect(m_slowUpdateStartTimer, &QTimer::timeout, this, &Manager::resetSlowUpdateStartTimer); + + updateSunTimings(false); + int diff; + if (m_mode == NightColorMode::Timings) { + // Timings mode is in local time + diff = QDateTime::currentDateTime().msecsTo(m_next.first); + } else { + diff = QDateTime::currentDateTimeUtc().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; + + QDateTime now = QDateTime::currentDateTimeUtc(); + bool isDay = daylight(); + int targetTemp = isDay ? m_dayTargetTemp : m_nightTargetTemp; + + if (m_prev.first == m_prev.second) { + // transition time is zero + commitGammaRamps(isDay ? m_dayTargetTemp : m_nightTargetTemp); + 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 / (qAbs(targetTemp - m_currentTemp) / TEMPERATURE_STEP); + 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::updateSunTimings(bool force) +{ + QDateTime todayNow = QDateTime::currentDateTimeUtc(); + + if (m_mode == NightColorMode::Timings) { + + QDateTime todayNowLocal = QDateTime::currentDateTime(); + + QDateTime morB = QDateTime(todayNowLocal.date(), m_morning); + QDateTime morE = morB.addSecs(m_trTime * 60); + QDateTime eveB = QDateTime(todayNowLocal.date(), m_evening); + QDateTime eveE = eveB.addSecs(m_trTime * 60); + + if (morB <= todayNowLocal && todayNowLocal < eveB) { + m_next = DateTimes(eveB, eveE); + m_prev = DateTimes(morB, morE); + } else if (todayNowLocal < 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); + } + 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.date().addDays(1), lat, lng, true); + } else { + // next is evening + m_prev = m_next; + m_next = getSunTimings(todayNow.date(), lat, lng, false); + } + } + + if (force || !checkAutomaticSunTimings()) { + // in case this fails, reset them + DateTimes morning = getSunTimings(todayNow.date(), lat, lng, true); + if (todayNow < morning.first) { + m_prev = getSunTimings(todayNow.date().addDays(-1), lat, lng, false); + m_next = morning; + } else { + DateTimes evening = getSunTimings(todayNow.date(), lat, lng, false); + if (todayNow < evening.first) { + m_prev = morning; + m_next = evening; + } else { + m_prev = evening; + m_next = getSunTimings(todayNow.date().addDays(1), lat, lng, true); + } + } + } +} + +DateTimes Manager::getSunTimings(QDate date, double latitude, double longitude, bool morning) const +{ + Times times = calculateSunTimings(date, 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. + bool beginDefined = !times.first.isNull(); + bool endDefined = !times.second.isNull(); + if (!beginDefined || !endDefined) { + if (beginDefined) { + times.second = times.first.addMSecs( FALLBACK_SLOW_UPDATE_TIME ); + } else if (endDefined) { + times.first = times.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. + times.first = morning ? QTime(6,0,0) : QTime(18,0,0); + times.second = times.first.addMSecs( FALLBACK_SLOW_UPDATE_TIME ); + } + } + return DateTimes(QDateTime(date, times.first, Qt::UTC), QDateTime(date, times.second, Qt::UTC)); +} + +bool Manager::checkAutomaticSunTimings() const +{ + if (m_prev.first.isValid() && m_prev.second.isValid() && + m_next.first.isValid() && m_next.second.isValid()) { + QDateTime todayNow = QDateTime::currentDateTimeUtc(); + 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_active) { + return NEUTRAL_TEMPERATURE; + } + + QDateTime todayNow = QDateTime::currentDateTimeUtc(); + + 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->getGammaRampSize(); + GammaRamp ramp(rampsize); + + /* + * The gamma calculation below is based on the Redshift app: + * https://github.com/jonls/redshift + */ + + // linear default state + for (int i = 0; i < rampsize; i++) { + uint16_t value = (double)i / rampsize * (UINT16_MAX + 1); + ramp.red[i] = value; + ramp.green[i] = value; + ramp.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++) { + ramp.red[i] = (double)ramp.red[i] / (UINT16_MAX+1) * whitePoint[0] * (UINT16_MAX+1); + ramp.green[i] = (double)ramp.green[i] / (UINT16_MAX+1) * whitePoint[1] * (UINT16_MAX+1); + ramp.blue[i] = (double)ramp.blue[i] / (UINT16_MAX+1) * whitePoint[2] * (UINT16_MAX+1); + } + + if (o->setGammaRamp(ramp)) { + m_currentTemp = 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 commited 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) + m_running = false; + cancelAllTimers(); + } + } + } +} + +QHash Manager::info() const +{ + return QHash { + { QStringLiteral("Available"), kwinApp()->platform()->supportsGammaControl() }, + + { 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 || 2 < mo) { + return false; + } + NightColorMode moM; + switch (mo) { + case 0: + moM = NightColorMode::Automatic; + break; + case 1: + moM = NightColorMode::Location; + break; + case 2: + moM = NightColorMode::Timings; + } + 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) { + m_active = active; + s->setActive(active); + } + + if (modeUpdate) { + m_mode = 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) +{ + 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()); +} + +} +} diff --git a/colorcorrection/manager.h b/colorcorrection/manager.h new file mode 100644 index 0000000..ae103ae --- /dev/null +++ b/colorcorrection/manager.h @@ -0,0 +1,147 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2017 Roman Gilg + +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, see . +*********************************************************************/ +#ifndef KWIN_COLORCORRECT_MANAGER_H +#define KWIN_COLORCORRECT_MANAGER_H + +#include "constants.h" +#include + +#include +#include +#include + +class QTimer; + +namespace KWin +{ + +class Platform; + +namespace ColorCorrect +{ + +typedef QPair DateTimes; +typedef QPair Times; + +class ColorCorrectDBusInterface; + + +enum NightColorMode { + // timings are based on provided location data + Automatic = 0, + // timings are based on fixed location data + Location, + // fixed timings + Timings +}; + +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); + + // for auto tests + void reparseConfigAndReset(); + +public Q_SLOTS: + void resetSlowUpdateStartTimer(); + void quickAdjust(); + +Q_SIGNALS: + void configChange(QHash data); + +private: + 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 updateSunTimings(bool force); + DateTimes getSunTimings(QDate date, double latitude, double longitude, bool morning) const; + bool checkAutomaticSunTimings() const; + bool daylight() const; + + void commitGammaRamps(int temperature); + + ColorCorrectDBusInterface *m_iface; + + bool m_active; + bool m_running = 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_dayTargetTemp = NEUTRAL_TEMPERATURE; + int m_nightTargetTemp = DEFAULT_NIGHT_TEMPERATURE; + + int m_failedCommitAttempts = 0; +}; + +} +} + +#endif // KWIN_COLORCORRECT_MANAGER_H diff --git a/colorcorrection/suncalc.cpp b/colorcorrection/suncalc.cpp new file mode 100644 index 0000000..71bb2e5 --- /dev/null +++ b/colorcorrection/suncalc.cpp @@ -0,0 +1,163 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2017 Roman Gilg + +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, see . +*********************************************************************/ +#include "suncalc.h" +#include "constants.h" + +#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 + +QPair calculateSunTimings(QDate prompt, double latitude, double longitude, bool morning) +{ + // calculations based on http://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 double juPrompt = prompt.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; + + QTime timeBegin, timeEnd; + + if (std::isnan(begin)) { + timeBegin = QTime(); + } else { + double timePart = begin - (int)begin; + timeBegin = QTime::fromMSecsSinceStartOfDay((int)( timePart * MSC_DAY )); + } + if (std::isnan(end)) { + timeEnd = QTime(); + } else { + double timePart = end - (int)end; + timeEnd = QTime::fromMSecsSinceStartOfDay((int)( timePart * MSC_DAY )); + } + + return QPair (timeBegin, timeEnd); +} + +} +} diff --git a/colorcorrection/suncalc.h b/colorcorrection/suncalc.h new file mode 100644 index 0000000..4cfcea0 --- /dev/null +++ b/colorcorrection/suncalc.h @@ -0,0 +1,47 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2017 Roman Gilg + +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, see . +*********************************************************************/ +#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(QDate prompt, double latitude, double longitude, bool morning); + + +} +} + +#endif // KWIN_SUNCALCULATOR_H diff --git a/composite.cpp b/composite.cpp new file mode 100644 index 0000000..932c914 --- /dev/null +++ b/composite.cpp @@ -0,0 +1,1206 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak + +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, see . +*********************************************************************/ +#include "composite.h" + +#include "dbusinterface.h" +#include "utils.h" +#include +#include "workspace.h" +#include "client.h" +#include "unmanaged.h" +#include "deleted.h" +#include "effects.h" +#include "overlaywindow.h" +#include "scene.h" +#include "screens.h" +#include "shadow.h" +#include "useractions.h" +#include "xcbutils.h" +#include "platform.h" +#include "shell_client.h" +#include "wayland_server.h" +#include "decorations/decoratedclient.h" + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +Q_DECLARE_METATYPE(KWin::Compositor::SuspendReason) + +namespace KWin +{ + +extern int currentRefreshRate(); + +CompositorSelectionOwner::CompositorSelectionOwner(const char *selection) : KSelectionOwner(selection, connection(), rootWindow()), owning(false) +{ + connect (this, SIGNAL(lostOwnership()), SLOT(looseOwnership())); +} + +void CompositorSelectionOwner::looseOwnership() +{ + owning = false; +} + +KWIN_SINGLETON_FACTORY_VARIABLE(Compositor, s_compositor) + +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_suspended(options->isUseCompositing() ? NoReasonSuspend : UserSuspend) + , cm_selection(NULL) + , vBlankInterval(0) + , fpsInterval(0) + , m_xrrRefreshRate(0) + , m_finishing(false) + , m_starting(false) + , m_timeSinceLastVBlank(0) + , m_scene(NULL) + , m_bufferSwapPending(false) + , m_composeAtSwapCompletion(false) +{ + qRegisterMetaType("Compositor::SuspendReason"); + connect(&compositeResetTimer, SIGNAL(timeout()), SLOT(restart())); + connect(options, &Options::configChanged, this, &Compositor::slotConfigChanged); + compositeResetTimer.setSingleShot(true); + nextPaintReference.invalidate(); // Initialize the timer + + // 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, SIGNAL(timeout()), SLOT(releaseCompositorSelection())); + + m_unusedSupportPropertyTimer.setInterval(compositorLostMessageDelay); + m_unusedSupportPropertyTimer.setSingleShot(true); + connect(&m_unusedSupportPropertyTimer, SIGNAL(timeout()), SLOT(deleteUnusedSupportProperties())); + + // delay the call to setup 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()) { + QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection); + } + connect(kwinApp()->platform(), &Platform::readyChanged, this, + [this] (bool ready) { + if (ready) { + setup(); + } else { + finish(); + } + }, Qt::QueuedConnection + ); + connect(kwinApp(), &Application::x11ConnectionAboutToBeDestroyed, this, + [this] { + delete cm_selection; + cm_selection = nullptr; + } + ); + + if (qEnvironmentVariableIsSet("KWIN_MAX_FRAMES_TESTED")) + m_framesToTestForSafety = qEnvironmentVariableIntValue("KWIN_MAX_FRAMES_TESTED"); + + // register DBus + new CompositorDBusInterface(this); +} + +Compositor::~Compositor() +{ + emit aboutToDestroy(); + finish(); + deleteUnusedSupportProperties(); + delete cm_selection; + s_compositor = NULL; +} + + +void Compositor::setup() +{ + if (hasScene()) + return; + 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; + } + m_starting = true; + + if (!options->isCompositingInitialized()) { + options->reloadCompositingSettings(true); + } + slotCompositingOptionsInitialized(); +} + +extern int screen_number; // main.cpp +extern bool is_multihead; + +void Compositor::slotCompositingOptionsInitialized() +{ + setupX11Support(); + + // 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(); + } + } + + 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 (auto type : qAsConst(supportedCompositors)) { + 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 == NULL || m_scene->initFailed()) { + qCCritical(KWIN_CORE) << "Failed to initialize compositing, compositing disabled"; + delete m_scene; + m_scene = NULL; + m_starting = false; + if (cm_selection) { + cm_selection->owning = false; + cm_selection->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; + } + + 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::restart); + emit sceneCreated(); + + if (Workspace::self()) { + startupWithWorkspace(); + } else { + connect(kwinApp(), &Application::workspaceCreated, this, &Compositor::startupWithWorkspace); + } +} + +void Compositor::claimCompositorSelection() +{ + if (!cm_selection) { + char selection_name[ 100 ]; + sprintf(selection_name, "_NET_WM_CM_S%d", Application::x11ScreenNumber()); + cm_selection = new CompositorSelectionOwner(selection_name); + connect(cm_selection, SIGNAL(lostOwnership()), SLOT(finish())); + } + + if (!cm_selection) // no X11 yet + return; + + if (!cm_selection->owning) { + cm_selection->claim(true); // force claiming + cm_selection->owning = true; + } +} + +void Compositor::setupX11Support() +{ + auto c = kwinApp()->x11Connection(); + if (!c) { + delete cm_selection; + cm_selection = nullptr; + return; + } + claimCompositorSelection(); + xcb_composite_redirect_subwindows(c, kwinApp()->x11RootWindow(), XCB_COMPOSITE_REDIRECT_MANUAL); +} + +void Compositor::startupWithWorkspace() +{ + if (!m_starting) { + return; + } + connect(kwinApp(), &Application::x11ConnectionChanged, this, &Compositor::setupX11Support, Qt::UniqueConnection); + Workspace::self()->markXStackingOrderAsDirty(); + Q_ASSERT(m_scene); + connect(workspace(), &Workspace::destroyed, this, [this] { compositeTimer.stop(); }); + setupX11Support(); + m_xrrRefreshRate = KWin::currentRefreshRate(); + 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) / m_xrrRefreshRate; + fpsInterval = qMax((fpsInterval / vBlankInterval) * vBlankInterval, vBlankInterval); + } else + vBlankInterval = milliToNano(1); // no sync - DO NOT set "0", would cause div-by-zero segfaults. + m_timeSinceLastVBlank = fpsInterval - (options->vBlankTime() + 1); // means "start now" - we don't have even a slight idea when the first vsync will occur + scheduleRepaint(); + kwinApp()->platform()->createEffectsHandler(this, m_scene); // sets also the 'effects' pointer + connect(Workspace::self(), &Workspace::deletedRemoved, m_scene, &Scene::windowDeleted); + connect(effects, SIGNAL(screenGeometryChanged(QSize)), SLOT(addRepaintFull())); + addRepaintFull(); + foreach (Client * c, Workspace::self()->clientList()) { + c->setupCompositing(); + c->getShadow(); + } + foreach (Client * c, Workspace::self()->desktopList()) + c->setupCompositing(); + foreach (Unmanaged * c, Workspace::self()->unmanagedList()) { + c->setupCompositing(); + c->getShadow(); + } + if (auto w = waylandServer()) { + const auto clients = w->clients(); + for (auto c : clients) { + c->setupCompositing(); + c->getShadow(); + } + const auto internalClients = w->internalClients(); + for (auto c : internalClients) { + c->setupCompositing(); + c->getShadow(); + } + } + + emit compositingToggled(true); + + m_starting = false; + if (m_releaseSelectionTimer.isActive()) { + m_releaseSelectionTimer.stop(); + } + + // render at least once + performCompositing(); +} + +void Compositor::scheduleRepaint() +{ + if (!compositeTimer.isActive()) + setCompositeTimer(); +} + +void Compositor::finish() +{ + if (!hasScene()) + return; + m_finishing = true; + m_releaseSelectionTimer.start(); + if (Workspace::self()) { + foreach (Client * c, Workspace::self()->clientList()) + m_scene->windowClosed(c, NULL); + foreach (Client * c, Workspace::self()->desktopList()) + m_scene->windowClosed(c, NULL); + foreach (Unmanaged * c, Workspace::self()->unmanagedList()) + m_scene->windowClosed(c, NULL); + foreach (Deleted * c, Workspace::self()->deletedList()) + m_scene->windowDeleted(c); + foreach (Client * c, Workspace::self()->clientList()) + c->finishCompositing(); + foreach (Client * c, Workspace::self()->desktopList()) + c->finishCompositing(); + foreach (Unmanaged * c, Workspace::self()->unmanagedList()) + c->finishCompositing(); + foreach (Deleted * c, Workspace::self()->deletedList()) + c->finishCompositing(); + if (auto c = kwinApp()->x11Connection()) { + xcb_composite_unredirect_subwindows(c, kwinApp()->x11RootWindow(), XCB_COMPOSITE_REDIRECT_MANUAL); + } + } + if (waylandServer()) { + foreach (ShellClient *c, waylandServer()->clients()) { + m_scene->windowClosed(c, nullptr); + } + foreach (ShellClient *c, waylandServer()->internalClients()) { + m_scene->windowClosed(c, nullptr); + } + foreach (ShellClient *c, waylandServer()->clients()) { + c->finishCompositing(); + } + foreach (ShellClient *c, waylandServer()->internalClients()) { + c->finishCompositing(); + } + } + delete effects; + effects = NULL; + delete m_scene; + m_scene = NULL; + compositeTimer.stop(); + repaints_region = QRegion(); + if (Workspace::self()) { + for (ClientList::ConstIterator it = Workspace::self()->clientList().constBegin(); + it != Workspace::self()->clientList().constEnd(); + ++it) { + // forward all opacity values to the frame in case there'll be other CM running + if ((*it)->opacity() != 1.0) { + NETWinInfo i(connection(), (*it)->frameId(), rootWindow(), 0, 0); + i.setOpacity(static_cast< unsigned long >((*it)->opacity() * 0xffffffff)); + } + } + // discard all Deleted windows (#152914) + while (!Workspace::self()->deletedList().isEmpty()) + Workspace::self()->deletedList().first()->discard(); + } + m_finishing = false; + emit compositingToggled(false); +} + +void Compositor::releaseCompositorSelection() +{ + if (hasScene() && !m_finishing) { + // compositor is up and running again, no need to release the selection + return; + } + if (m_starting) { + // currently still starting the compositor, it might fail, so restart the timer to test again + m_releaseSelectionTimer.start(); + return; + } + + if (m_finishing) { + // still shutting down, a restart might follow, so restart the timer to test again + m_releaseSelectionTimer.start(); + return; + } + qCDebug(KWIN_CORE) << "Releasing compositor selection"; + if (cm_selection) { + cm_selection->owning = false; + cm_selection->release(); + } +} + +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_starting) { + // currently still starting the compositor + m_unusedSupportPropertyTimer.start(); + return; + } + if (m_finishing) { + // still shutting down, a restart might follow + m_unusedSupportPropertyTimer.start(); + return; + } + if (const auto c = kwinApp()->x11Connection()) { + foreach (const xcb_atom_t &atom, m_unusedSupportProperties) { + // remove property from root window + xcb_delete_property(c, kwinApp()->x11RootWindow(), atom); + } + } +} + +void Compositor::slotConfigChanged() +{ + if (!m_suspended) { + setup(); + if (effects) // setupCompositing() may fail + effects->reconfigure(); + addRepaintFull(); + } else + finish(); +} + +void Compositor::slotReinitialize() +{ + // Reparse config. Config options will be reloaded by setup() + kwinApp()->config()->reparseConfiguration(); + + // Restart compositing + finish(); + // resume compositing if suspended + m_suspended = NoReasonSuspend; + options->setCompositingInitialized(false); + setup(); + + if (effects) { // setup() may fail + effects->reconfigure(); + } +} + +// for the shortcut +void Compositor::slotToggleCompositing() +{ + if (kwinApp()->platform()->requiresCompositing()) { + // we are not allowed to turn on/off compositing + return; + } + if (m_suspended) { // direct user call; clear all bits + resume(AllReasonSuspend); + } else { // but only set the user one (sufficient to suspend) + suspend(UserSuspend); + } +} + +void Compositor::updateCompositeBlocking() +{ + updateCompositeBlocking(NULL); +} + +void Compositor::updateCompositeBlocking(Client *c) +{ + if (kwinApp()->platform()->requiresCompositing()) { + return; + } + if (c) { // if c == 0 we just check if we can resume + if (c->isBlockingCompositing()) { + if (!(m_suspended & BlockRuleSuspend)) // do NOT attempt to call suspend(true); from within the eventchain! + QMetaObject::invokeMethod(this, "suspend", Qt::QueuedConnection, Q_ARG(Compositor::SuspendReason, BlockRuleSuspend)); + } + } + else if (m_suspended & BlockRuleSuspend) { // lost a client and we're blocked - can we resume? + bool resume = true; + for (ClientList::ConstIterator it = Workspace::self()->clientList().constBegin(); it != Workspace::self()->clientList().constEnd(); ++it) { + if ((*it)->isBlockingCompositing()) { + resume = false; + break; + } + } + if (resume) { // do NOT attempt to call suspend(false); from within the eventchain! + QMetaObject::invokeMethod(this, "resume", Qt::QueuedConnection, Q_ARG(Compositor::SuspendReason, BlockRuleSuspend)); + } + } +} + +void Compositor::suspend(Compositor::SuspendReason reason) +{ + if (kwinApp()->platform()->requiresCompositing()) { + return; + } + Q_ASSERT(reason != NoReasonSuspend); + m_suspended |= reason; + if (reason & KWin::Compositor::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); + } + } + finish(); +} + +void Compositor::resume(Compositor::SuspendReason reason) +{ + Q_ASSERT(reason != NoReasonSuspend); + m_suspended &= ~reason; + setup(); // signal "toggled" is eventually emitted from within setup +} + +void Compositor::restart() +{ + if (hasScene()) { + finish(); + QTimer::singleShot(0, this, SLOT(setup())); + } +} + +void Compositor::addRepaint(int x, int y, int w, int h) +{ + if (!hasScene()) + return; + repaints_region += QRegion(x, y, w, h); + scheduleRepaint(); +} + +void Compositor::addRepaint(const QRect& r) +{ + if (!hasScene()) + return; + repaints_region += r; + scheduleRepaint(); +} + +void Compositor::addRepaint(const QRegion& r) +{ + if (!hasScene()) + return; + repaints_region += r; + scheduleRepaint(); +} + +void Compositor::addRepaintFull() +{ + if (!hasScene()) + 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() +{ + assert(!m_bufferSwapPending); + + m_bufferSwapPending = true; +} + +void Compositor::bufferSwapComplete() +{ + assert(m_bufferSwapPending); + m_bufferSwapPending = false; + + if (m_composeAtSwapCompletion) { + m_composeAtSwapCompletion = false; + performCompositing(); + } +} + +void Compositor::performCompositing() +{ + if (m_scene->usesOverlayWindow() && !isOverlayWindowVisible()) + return; // nothing is visible anyway + + // 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 + ToplevelList windows = Workspace::self()->xStackingOrder(); + ToplevelList damaged; + + // Reset the damage state of each window and fetch the damage region + // without waiting for a reply + foreach (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 + foreach (EffectWindow *c, static_cast(effects)->elevatedWindows()) { + Toplevel* t = static_cast< EffectWindowImpl* >(c)->window(); + windows.removeAll(t); + windows.append(t); + } + + // Get the replies + foreach (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" + m_timeSinceStart += m_timeSinceLastVBlank; + // 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. + foreach (Toplevel *t, windows) { + if (!t->readyForPainting()) { + windows.removeAll(t); + } + if (waylandServer() && waylandServer()->isScreenLocked()) { + if(!t->isLockScreen() && !t->isInputMethod()) { + windows.removeAll(t); + } + } + } + + 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); + } + } + m_timeSinceStart += m_timeSinceLastVBlank; + + if (waylandServer()) { + for (Toplevel *win : qAsConst(damaged)) { + if (auto surface = win->surface()) { + surface->frameRendered(m_timeSinceStart); + } + } + } + + compositeTimer.stop(); // stop here to ensure *we* cause the next repaint schedule - not some effect through m_scene->paint() + + // 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()->desktopList())) { + return true; + } + if (repaintsPending(Workspace::self()->unmanagedList())) { + return true; + } + if (repaintsPending(Workspace::self()->deletedList())) { + return true; + } + if (auto w = waylandServer()) { + const auto &clients = w->clients(); + auto test = [] (ShellClient *c) { + return c->readyForPainting() && !c->repaints().isEmpty(); + }; + if (std::any_of(clients.begin(), clients.end(), test)) { + return true; + } + const auto &internalClients = w->internalClients(); + auto internalTest = [] (ShellClient *c) { + return c->isShown(true) && !c->repaints().isEmpty(); + }; + if (std::any_of(internalClients.begin(), internalClients.end(), internalTest)) { + return true; + } + } + return false; +} + +void Compositor::setCompositeResetTimer(int msecs) +{ + compositeResetTimer.start(msecs); +} + +void Compositor::setCompositeTimer() +{ + if (!hasScene()) // should not really happen, but there may be e.g. some damage events still pending + return; + if (m_starting || !Workspace::self()) { + 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 + padding = vBlankInterval - (padding%vBlankInterval); // -> align to next vblank + } else { // -> align to the next maxFps tick + padding = ((vBlankInterval - padding%vBlankInterval) + (fpsInterval/vBlankInterval-1)*vBlankInterval); + // "remaining time of the first vsync" + "time for the other vsyncs of the frame" + } + + if (padding < options->vBlankTime()) { // we'll likely miss this frame + waitTime = nanoToMilli(padding + vBlankInterval - options->vBlankTime()); // so we add one + } 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) { + waitTime = 1; // will ensure we don't block out the eventloop - the system's just not faster ... + } + }/* 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 { + waitTime = 1; // ... "0" would be sufficient, but the compositor isn't the WMs only task + } + } + compositeTimer.start(qMin(waitTime, 250u), this); // force 4fps minimum +} + +bool Compositor::isActive() +{ + return !m_finishing && hasScene(); +} + +bool Compositor::checkForOverlayWindow(WId w) const +{ + if (!hasScene()) { + // no scene, so it cannot be the overlay window + return false; + } + if (!m_scene->overlayWindow()) { + // no overlay window, it cannot be the overlay + return false; + } + // and compare the window ID's + return w == m_scene->overlayWindow()->window(); +} + +bool Compositor::isOverlayWindowVisible() const +{ + if (!hasScene()) { + return false; + } + if (!m_scene->overlayWindow()) { + return false; + } + return m_scene->overlayWindow()->isVisible(); +} + +/***************************************************** + * Workspace + ****************************************************/ + +bool Workspace::compositing() const +{ + return m_compositor && m_compositor->hasScene(); +} + +//**************************************** +// Toplevel +//**************************************** + +bool Toplevel::setupCompositing() +{ + if (!compositing()) + return false; + + if (damage_handle != XCB_NONE) + return false; + + if (kwinApp()->operationMode() == Application::OperationModeX11 && !surface()) { + damage_handle = xcb_generate_id(connection()); + xcb_damage_create(connection(), damage_handle, frameId(), XCB_DAMAGE_REPORT_LEVEL_NON_EMPTY); + } + + damage_region = QRegion(0, 0, width(), height()); + effect_window = new EffectWindowImpl(this); + + Compositor::self()->scene()->windowAdded(this); + + // With unmanaged windows there is a race condition between the client painting the window + // and us setting up damage tracking. If the client wins we won't get a damage event even + // though the window has been painted. To avoid this we mark the whole window as damaged + // and schedule a repaint immediately after creating the damage object. + if (dynamic_cast(this)) + addDamageFull(); + + return true; +} + +void Toplevel::finishCompositing(ReleaseReason releaseReason) +{ + if (kwinApp()->operationMode() == Application::OperationModeX11 && damage_handle == XCB_NONE) + return; + if (effect_window->window() == this) { // otherwise it's already passed to Deleted, don't free data + discardWindowPixmap(); + delete effect_window; + } + + if (damage_handle != XCB_NONE && + releaseReason != ReleaseReason::Destroyed) { + xcb_damage_destroy(connection(), damage_handle); + } + + damage_handle = XCB_NONE; + damage_region = QRegion(); + repaints_region = QRegion(); + effect_window = NULL; +} + +void Toplevel::discardWindowPixmap() +{ + addDamageFull(); + if (effectWindow() != NULL && effectWindow()->sceneWindow() != NULL) + effectWindow()->sceneWindow()->pixmapDiscarded(); +} + +void Toplevel::damageNotifyEvent() +{ + m_isDamaged = true; + + // Note: The rect is supposed to specify the damage extents, + // but we don't know it at this point. No one who connects + // to this signal uses the rect however. + emit damaged(this, QRect()); +} + +bool Toplevel::compositing() const +{ + if (!Workspace::self()) { + return false; + } + return Workspace::self()->compositing(); +} + +void Client::damageNotifyEvent() +{ + if (syncRequest.isPending && isResize()) { + emit damaged(this, QRect()); + m_isDamaged = true; + return; + } + + if (!ready_for_painting) { // avoid "setReadyForPainting()" function calling overhead + if (syncRequest.counter == XCB_NONE) { // cannot detect complete redraw, consider done now + setReadyForPainting(); + setupWindowManagementInterface(); + } + } + + Toplevel::damageNotifyEvent(); +} + +bool Toplevel::resetAndFetchDamage() +{ + if (!m_isDamaged) + return false; + + if (damage_handle == XCB_NONE) { + m_isDamaged = false; + return true; + } + + xcb_connection_t *conn = connection(); + + // Create a new region and copy the damage region to it, + // resetting the damaged state. + xcb_xfixes_region_t region = xcb_generate_id(conn); + xcb_xfixes_create_region(conn, region, 0, 0); + xcb_damage_subtract(conn, damage_handle, 0, region); + + // Send a fetch-region request and destroy the region + m_regionCookie = xcb_xfixes_fetch_region_unchecked(conn, region); + xcb_xfixes_destroy_region(conn, region); + + m_isDamaged = false; + m_damageReplyPending = true; + + return m_damageReplyPending; +} + +void Toplevel::getDamageRegionReply() +{ + if (!m_damageReplyPending) + return; + + m_damageReplyPending = false; + + // Get the fetch-region reply + xcb_xfixes_fetch_region_reply_t *reply = + xcb_xfixes_fetch_region_reply(connection(), m_regionCookie, 0); + + if (!reply) + return; + + // Convert the reply to a QRegion + int count = xcb_xfixes_fetch_region_rectangles_length(reply); + QRegion region; + + if (count > 1 && count < 16) { + xcb_rectangle_t *rects = xcb_xfixes_fetch_region_rectangles(reply); + + QVector qrects; + qrects.reserve(count); + + for (int i = 0; i < count; i++) + qrects << QRect(rects[i].x, rects[i].y, rects[i].width, rects[i].height); + + region.setRects(qrects.constData(), count); + } else + region += QRect(reply->extents.x, reply->extents.y, + reply->extents.width, reply->extents.height); + + damage_region += region; + repaints_region += region; + + free(reply); +} + +void Toplevel::addDamageFull() +{ + if (!compositing()) + return; + + damage_region = rect(); + repaints_region |= rect(); + + emit damaged(this, rect()); +} + +void Toplevel::resetDamage() +{ + damage_region = QRegion(); +} + +void Toplevel::addRepaint(const QRect& r) +{ + if (!compositing()) { + return; + } + repaints_region += r; + emit needsRepaint(); +} + +void Toplevel::addRepaint(int x, int y, int w, int h) +{ + QRect r(x, y, w, h); + addRepaint(r); +} + +void Toplevel::addRepaint(const QRegion& r) +{ + if (!compositing()) { + return; + } + repaints_region += r; + emit needsRepaint(); +} + +void Toplevel::addLayerRepaint(const QRect& r) +{ + if (!compositing()) { + return; + } + layer_repaints_region += r; + emit needsRepaint(); +} + +void Toplevel::addLayerRepaint(int x, int y, int w, int h) +{ + QRect r(x, y, w, h); + addLayerRepaint(r); +} + +void Toplevel::addLayerRepaint(const QRegion& r) +{ + if (!compositing()) + return; + layer_repaints_region += r; + emit needsRepaint(); +} + +void Toplevel::addRepaintFull() +{ + repaints_region = visibleRect().translated(-pos()); + emit needsRepaint(); +} + +void Toplevel::resetRepaints() +{ + repaints_region = QRegion(); + layer_repaints_region = QRegion(); +} + +void Toplevel::addWorkspaceRepaint(int x, int y, int w, int h) +{ + addWorkspaceRepaint(QRect(x, y, w, h)); +} + +void Toplevel::addWorkspaceRepaint(const QRect& r2) +{ + if (!compositing()) + return; + Compositor::self()->addRepaint(r2); +} + +//**************************************** +// Client +//**************************************** + +bool Client::setupCompositing() +{ + if (!Toplevel::setupCompositing()){ + return false; + } + if (isDecorated()) { + decoratedClient()->destroyRenderer(); + } + updateVisibility(); // for internalKeep() + return true; +} + +void Client::finishCompositing(ReleaseReason releaseReason) +{ + Toplevel::finishCompositing(releaseReason); + updateVisibility(); + if (!deleting) { + if (isDecorated()) { + decoratedClient()->destroyRenderer(); + } + } + // for safety in case KWin is just resizing the window + resetHaveResizeEffect(); +} + +} // namespace diff --git a/composite.h b/composite.h new file mode 100644 index 0000000..463da5f --- /dev/null +++ b/composite.h @@ -0,0 +1,240 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Arthur Arlt +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef KWIN_COMPOSITE_H +#define KWIN_COMPOSITE_H +// KWin +#include +// KDE +#include +// Qt +#include +#include +#include +#include +#include + +namespace KWin { + +class Client; +class Scene; + +class CompositorSelectionOwner : public KSelectionOwner +{ + Q_OBJECT +public: + CompositorSelectionOwner(const char *selection); +private: + friend class Compositor; + bool owning; +private Q_SLOTS: + void looseOwnership(); +}; + +class KWIN_EXPORT Compositor : public QObject { + Q_OBJECT +public: + enum SuspendReason { NoReasonSuspend = 0, UserSuspend = 1<<0, BlockRuleSuspend = 1<<1, ScriptSuspend = 1<<2, AllReasonSuspend = 0xff }; + Q_DECLARE_FLAGS(SuspendReasons, SuspendReason) + ~Compositor(); + // 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); + /** + * Whether the Compositor is active. That is a Scene is present and the Compositor is + * not shutting down itself. + **/ + bool isActive(); + int xrrRefreshRate() const { + return m_xrrRefreshRate; + } + void setCompositeResetTimer(int msecs); + + bool hasScene() const { + return m_scene != NULL; + } + + /** + * 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; + + Scene *scene() { + return m_scene; + } + + /** + * @brief Checks whether the Compositor has already been created by the Workspace. + * + * This method can be used to check whether self will return the Compositor instance or @c null. + * + * @return bool @c true if the Compositor has been created, @c false otherwise + **/ + static bool isCreated() { + return s_compositor != NULL; + } + /** + * @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 != NULL && s_compositor->isActive(); + } + + // for delayed supportproperty management of effects + void keepSupportProperty(xcb_atom_t atom); + void removeSupportProperty(xcb_atom_t atom); + +public Q_SLOTS: + void addRepaintFull(); + /** + * @brief Suspends the Compositor if it is currently active. + * + * Note: it is possible that the Compositor is not able to suspend. Use @link isActive to check + * whether the Compositor has been suspended. + * + * @return void + * @see resume + * @see isActive + **/ + void suspend(Compositor::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 @link isActive to check + * whether the Compositor has been resumed. Also check @link isCompositingPossible and + * @link 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(Compositor::SuspendReason reason); + /** + * Actual slot to perform the toggling compositing. + * That is if the Compositor is suspended it will be resumed and if the Compositor is active + * it will be suspended. + * Invoked primarily by the keybinding. + * TODO: make private slot + **/ + void slotToggleCompositing(); + /** + * Re-initializes the Compositor completely. + * Connected to the D-Bus signal org.kde.KWin /KWin reinitCompositing + **/ + void slotReinitialize(); + /** + * Schedules a new repaint if no repaint is currently scheduled. + **/ + void scheduleRepaint(); + void updateCompositeBlocking(); + void updateCompositeBlocking(KWin::Client* c); + + /** + * 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(); + +Q_SIGNALS: + void compositingToggled(bool active); + void aboutToDestroy(); + void sceneCreated(); + +protected: + void timerEvent(QTimerEvent *te); + +private Q_SLOTS: + void setup(); + /** + * Called from setupCompositing() when the CompositingPrefs are ready. + **/ + void slotCompositingOptionsInitialized(); + void finish(); + /** + * Restarts the Compositor if running. + * That is the Compositor will be stopped and started again. + **/ + void restart(); + void performCompositing(); + void slotConfigChanged(); + void releaseCompositorSelection(); + void deleteUnusedSupportProperties(); + +private: + void claimCompositorSelection(); + void setCompositeTimer(); + bool windowRepaintsPending() const; + /** + * Continues the startup after Scene And Workspace are created + **/ + void startupWithWorkspace(); + void setupX11Support(); + + /** + * Whether the Compositor is currently suspended, 8 bits encoding the reason + **/ + SuspendReasons m_suspended; + + QBasicTimer compositeTimer; + CompositorSelectionOwner* cm_selection; + QTimer m_releaseSelectionTimer; + QList m_unusedSupportProperties; + QTimer m_unusedSupportPropertyTimer; + qint64 vBlankInterval, fpsInterval; + int m_xrrRefreshRate; + QElapsedTimer nextPaintReference; + QRegion repaints_region; + + QTimer compositeResetTimer; // for compressing composite resets + bool m_finishing; // finish() sets this variable while shutting down + bool m_starting; // start() sets this variable while starting + qint64 m_timeSinceLastVBlank; + qint64 m_timeSinceStart = 0; + Scene *m_scene; + bool m_bufferSwapPending; + bool m_composeAtSwapCompletion; + int m_framesToTestForSafety = 3; + + KWIN_SINGLETON_VARIABLE(Compositor, s_compositor) +}; +} + +# endif // KWIN_COMPOSITE_H diff --git a/config-kwin.h.cmake b/config-kwin.h.cmake new file mode 100644 index 0000000..8f68080 --- /dev/null +++ b/config-kwin.h.cmake @@ -0,0 +1,42 @@ +#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_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 +#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 diff --git a/cursor.cpp b/cursor.cpp new file mode 100644 index 0000000..b155b34 --- /dev/null +++ b/cursor.cpp @@ -0,0 +1,477 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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 +#include +// Qt +#include +#include +#include +#include +// xcb +#include + +namespace KWin +{ +Cursor *Cursor::s_self = nullptr; + +Cursor::Cursor(QObject *parent) + : QObject(parent) + , m_mousePollingCounter(0) + , m_cursorTrackingCounter(0) + , m_themeName("default") + , m_themeSize(24) +{ + s_self = this; + loadThemeSettings(); + QDBusConnection::sessionBus().connect(QString(), QStringLiteral("/KGlobalSettings"), QStringLiteral("org.kde.KGlobalSettings"), + QStringLiteral("notifyChange"), this, SLOT(slotKGlobalSettingsNotifyChange(int,int))); +} + +Cursor::~Cursor() +{ + s_self = NULL; +} + +void Cursor::loadThemeSettings() +{ + QString themeName = QString::fromUtf8(qgetenv("XCURSOR_THEME")); + bool ok = false; + // XCURSOR_SIZE might not be set (e.g. by startkde) + const uint themeSize = qEnvironmentVariableIntValue("XCURSOR_SIZE", &ok); + if (!themeName.isEmpty() && ok) { + updateTheme(themeName, themeSize); + return; + } + // didn't get from environment variables, read from config file + loadThemeFromKConfig(); +} + +void Cursor::loadThemeFromKConfig() +{ + KConfigGroup mousecfg(kwinApp()->inputConfig(), "Mouse"); + const QString themeName = mousecfg.readEntry("cursorTheme", "default"); + const uint themeSize = mousecfg.readEntry("cursorSize", 0); + 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) +{ + Q_UNUSED(arg) + if (type == 5 /*CursorChanged*/) { + kwinApp()->inputConfig()->reparseConfiguration(); + loadThemeFromKConfig(); + // sync to environment + qputenv("XCURSOR_THEME", m_themeName.toUtf8()); + qputenv("XCURSOR_SIZE", QByteArray::number(m_themeSize)); + } +} + +QPoint Cursor::pos() +{ + s_self->doGetPos(); + return s_self->m_pos; +} + +void Cursor::setPos(const QPoint &pos) +{ + // first query the current pos to not warp to the already existing pos + if (pos == Cursor::pos()) { + return; + } + s_self->m_pos = pos; + s_self->doSetPos(); +} + +void Cursor::setPos(int x, int y) +{ + Cursor::setPos(QPoint(x, y)); +} + +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 s_self->getX11Cursor(shape); +} + +xcb_cursor_t Cursor::x11Cursor(const QByteArray &name) +{ + return s_self->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) const +{ + 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(); +} + +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(); + } +} + +InputRedirectionCursor::InputRedirectionCursor(QObject *parent) + : Cursor(parent) + , m_currentButtons(Qt::NoButton) +{ + connect(input(), SIGNAL(globalPointerChanged(QPointF)), SLOT(slotPosChanged(QPointF))); + connect(input(), SIGNAL(pointerButtonStateChanged(uint32_t,InputRedirection::PointerButtonState)), + SLOT(slotPointerButtonChanged())); +#ifndef KCMRULES + connect(input(), &InputRedirection::keyboardModifiersChanged, + this, &InputRedirectionCursor::slotModifiersChanged); +#endif +} + +InputRedirectionCursor::~InputRedirectionCursor() +{ +} + +void InputRedirectionCursor::doSetPos() +{ + if (input()->supportsPointerWarping()) { + input()->warpPointer(currentPos()); + } + slotPosChanged(input()->globalPointer()); + emit posChanged(currentPos()); +} + +void InputRedirectionCursor::slotPosChanged(const QPointF &pos) +{ + const QPoint oldPos = currentPos(); + updatePos(pos.toPoint()); + emit mouseChanged(pos.toPoint(), oldPos, m_currentButtons, m_currentButtons, + input()->keyboardModifiers(), input()->keyboardModifiers()); +} + +void InputRedirectionCursor::slotModifiersChanged(Qt::KeyboardModifiers mods, Qt::KeyboardModifiers oldMods) +{ + emit mouseChanged(currentPos(), currentPos(), m_currentButtons, m_currentButtons, mods, oldMods); +} + +void InputRedirectionCursor::slotPointerButtonChanged() +{ + const Qt::MouseButtons oldButtons = m_currentButtons; + m_currentButtons = input()->qtButtonStates(); + const QPoint pos = currentPos(); + emit mouseChanged(pos, pos, m_currentButtons, oldButtons, input()->keyboardModifiers(), input()->keyboardModifiers()); +} + +void InputRedirectionCursor::doStartCursorTracking() +{ +#ifndef KCMRULES + connect(kwinApp()->platform(), &Platform::cursorChanged, this, &Cursor::cursorChanged); +#endif +} + +void InputRedirectionCursor::doStopCursorTracking() +{ +#ifndef KCMRULES + disconnect(kwinApp()->platform(), &Platform::cursorChanged, this, &Cursor::cursorChanged); +#endif +} + +} // namespace diff --git a/cursor.h b/cursor.h new file mode 100644 index 0000000..dc4eefa --- /dev/null +++ b/cursor.h @@ -0,0 +1,303 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_CURSOR_H +#define KWIN_CURSOR_H +// kwin +#include +// Qt +#include +#include +#include +// xcb +#include + +class QTimer; + +namespace KWin +{ + +namespace ExtendedCursor { +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 +}; +} +/** + * Extension of Qt::CursorShape with values not currently present there + */ + + +/** + * @brief Wrapper round Qt::CursorShape with extensions enums into a single entity + */ +class KWIN_EXPORT CursorShape { +public: + 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: + virtual ~Cursor(); + void startMousePolling(); + void stopMousePolling(); + /** + * @brief Enables tracking changes of cursor images. + * + * After enabling cursor change tracking the signal @link cursorChanged will be emitted + * whenever a change to the cursor image is recognized. + * + * Use @link 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 @link 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 + **/ + QVector cursorAlternativeNames(const QByteArray &name) const; + + /** + * 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 @link currentPos which is not performing a check + * for update. + **/ + static QPoint pos(); + /** + * Warps the mouse cursor to new @p pos. + **/ + static void setPos(const QPoint &pos); + static void setPos(int x, int y); + static 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 spcification + **/ + static xcb_cursor_t x11Cursor(const QByteArray &name); + +Q_SIGNALS: + void posChanged(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 @link startCursorTracking. + * + * @see startCursorTracking + * @see stopCursorTracking + */ + void cursorChanged(); + void themeChanged(); + +protected: + /** + * Called from @link 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 @link 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 @link pos() to allow syncing the internal position with the underlying + * system's cursor position. + **/ + virtual void doGetPos(); + /** + * Called from @link 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 @link 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 @link 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 @link 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 @link 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 + * @link 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; + int m_mousePollingCounter; + int m_cursorTrackingCounter; + QString m_themeName; + int m_themeSize; + + KWIN_SINGLETON(Cursor) +}; + +/** + * @brief Implementation using the InputRedirection framework to get pointer positions. + * + * Does not support warping of cursor. + * + */ +class InputRedirectionCursor : public Cursor +{ + Q_OBJECT +public: + explicit InputRedirectionCursor(QObject *parent); + virtual ~InputRedirectionCursor(); +protected: + virtual void doSetPos(); + virtual void doStartCursorTracking(); + virtual void doStopCursorTracking(); +private Q_SLOTS: + void slotPosChanged(const QPointF &pos); + void slotPointerButtonChanged(); + void slotModifiersChanged(Qt::KeyboardModifiers mods, Qt::KeyboardModifiers oldMods); +private: + Qt::MouseButtons m_currentButtons; + friend class Cursor; +}; + +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; +} + +} + +#endif // KWIN_CURSOR_H diff --git a/data/CMakeLists.txt b/data/CMakeLists.txt new file mode 100644 index 0000000..0fc2c67 --- /dev/null +++ b/data/CMakeLists.txt @@ -0,0 +1,11 @@ +########### next target ############### +add_executable( kwin5_update_default_rules update_default_rules.cpp) + +target_link_libraries( kwin5_update_default_rules Qt5::Core Qt5::DBus KF5::ConfigCore ) + +install(TARGETS kwin5_update_default_rules DESTINATION ${LIB_INSTALL_DIR}/kconf_update_bin/ ) + +########### install files ############### + +install( FILES org_kde_kwin.categories DESTINATION ${KDE_INSTALL_CONFDIR} ) + diff --git a/data/org_kde_kwin.categories b/data/org_kde_kwin.categories new file mode 100644 index 0000000..a256fa2 --- /dev/null +++ b/data/org_kde_kwin.categories @@ -0,0 +1,21 @@ +kwin_core KWin Core +kwineffects KWin Effects +libkwineffects KWin Effects Library +libkwinglutils KWin OpenGL utility Library +libkwinxrenderutils KWin XRender utility Library +kwin_wayland_drm KWin Wayland (DRM backend) +kwin_wayland_framebuffer KWin Wayland (Framebuffer backend) +kwin_wayland_hwcomposer KWin Wayland (hwcomposer backend) +kwin_wayland_backend KWin Wayland (Wayland backend) +kwin_wayland_x11windowed KWin Wayland (X11 backend) +kwin_platform_x11_standalone KWin X11 Standalone Platform +kwin_libinput KWin Libinput Integration +kwin_tabbox KWin Window Switcher +kwin_decorations KWin Decorations +kwin_scripting KWin Scripting +aurorae KWin Aurorae Window Decoration Engine +kwin_xkbcommon KWin xkbcommon integration +kwin_qpa_plugin KWin QtPlatformAbstraction plugin +kwin_scene_xrender KWin XRender based compositor scene plugin +kwin_scene_qpainter KWin QPainter based compositor scene plugin +kwin_scene_opengl KWin OpenGL based compositor scene plugins diff --git a/data/update_default_rules.cpp b/data/update_default_rules.cpp new file mode 100644 index 0000000..b607c88 --- /dev/null +++ b/data/update_default_rules.cpp @@ -0,0 +1,69 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2005 Lubos Lunak + +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, see . +*********************************************************************/ + +// 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" ); + 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..9bfd995 --- /dev/null +++ b/dbusinterface.cpp @@ -0,0 +1,318 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ + +// own +#include "dbusinterface.h" +#include "compositingadaptor.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(); +} + +QVariantMap DBusInterface::queryWindowInfo() +{ + m_replyQueryWindowInfo = message(); + setDelayedReply(true); + kwinApp()->platform()->startInteractiveWindowSelection( + [this] (Toplevel *t) { + if (auto c = qobject_cast(t)) { + const QVariantMap ret{ + {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("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} + }; + QDBusConnection::sessionBus().send(m_replyQueryWindowInfo.createReply(ret)); + } else { + QDBusConnection::sessionBus().send(m_replyQueryWindowInfo.createErrorReply(QString(), QString())); + } + } + ); + return QVariantMap{}; +} + +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"), m_compositor, SLOT(slotReinitialize())); +} + +QString CompositorDBusInterface::compositingNotPossibleReason() const +{ + return kwinApp()->platform()->compositingNotPossibleReason(); +} + +QString CompositorDBusInterface::compositingType() const +{ + if (!m_compositor->hasScene()) { + 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() +{ + m_compositor->resume(Compositor::ScriptSuspend); +} + +void CompositorDBusInterface::suspend() +{ + m_compositor->suspend(Compositor::ScriptSuspend); +} + +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; +} + +} // namespace diff --git a/dbusinterface.h b/dbusinterface.h new file mode 100644 index 0000000..dd23d7c --- /dev/null +++ b/dbusinterface.h @@ -0,0 +1,174 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef KWIN_DBUS_INTERFACE_H +#define KWIN_DBUS_INTERFACE_H + +#include +#include + +namespace KWin +{ + +class Compositor; + +/** + * @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); + virtual ~DBusInterface(); + +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(); + +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); + virtual ~CompositorDBusInterface() = 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 @link 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 @link isActive to check + * whether the Compositor has been resumed. Also check @link isCompositingPossible and + * @link 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(); + +Q_SIGNALS: + void compositingToggled(bool active); + +private: + Compositor *m_compositor; +}; + +} // namespace + +#endif // KWIN_DBUS_INTERFACE_H diff --git a/debug_console.cpp b/debug_console.cpp new file mode 100644 index 0000000..a556bb3 --- /dev/null +++ b/debug_console.cpp @@ -0,0 +1,1525 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "debug_console.h" +#include "composite.h" +#include "client.h" +#include "input_event.h" +#include "main.h" +#include "scene.h" +#include "shell_client.h" +#include "unmanaged.h" +#include "wayland_server.h" +#include "workspace.h" +#include "keyboard_input.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(QStringLiteral("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())); + text.append(tableRow(i18nc("The code as read from the input device", "Scan code"), event->nativeScanCode())); + 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(quint32 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(quint32 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(quint32 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(); +} + +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.type()) { + 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("KWayland::Server::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_waylandInternalId = 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) { + m_shellClients.append(c); + } + const auto internals = waylandServer()->internalClients(); + for (auto c : internals) { + m_internalClients.append(c); + } + // TODO: that only includes windows getting shown, not those which are only created + connect(waylandServer(), &WaylandServer::shellClientAdded, this, + [this] (ShellClient *c) { + if (c->isInternal()) { + add(s_waylandInternalId -1, m_internalClients, c); + } else { + add(s_waylandClientId -1, m_shellClients, c); + } + } + ); + connect(waylandServer(), &WaylandServer::shellClientRemoved, this, + [this] (ShellClient *c) { + remove(s_waylandInternalId -1, m_internalClients, c); + remove(s_waylandClientId -1, m_shellClients, c); + } + ); + } + const auto x11Clients = workspace()->clientList(); + for (auto c : x11Clients) { + m_x11Clients.append(c); + } + const auto x11DesktopClients = workspace()->desktopList(); + for (auto c : x11DesktopClients) { + m_x11Clients.append(c); + } + connect(workspace(), &Workspace::clientAdded, this, + [this] (Client *c) { + add(s_x11ClientId -1, m_x11Clients, c); + } + ); + connect(workspace(), &Workspace::clientRemoved, this, + [this] (AbstractClient *ac) { + Client *c = qobject_cast(ac); + if (!c) { + return; + } + remove(s_x11ClientId -1, m_x11Clients, c); + } + ); + + 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); + } + ); +} + +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_shellClients.count(); + case s_waylandInternalId: + 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::shellClient); + } else if (parent.internalId() < s_idDistance * (s_waylandInternalId + 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_shellClients, s_waylandClientId); + case s_waylandInternalId: + return indexForClient(row, column, m_internalClients, s_waylandInternalId); + 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::shellClient); + } else if (parent.internalId() < s_idDistance * (s_waylandInternalId + 1)) { + return indexForProperty(row, column, parent, &DebugConsoleModel::internalClient); + } + + return QModelIndex(); +} + +QModelIndex DebugConsoleModel::parent(const QModelIndex &child) const +{ + if (child.internalId() <= s_waylandInternalId) { + 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_waylandInternalId + 1)) { + return createIndex(parentId - (s_idDistance * s_waylandInternalId), 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_waylandInternalId + 1)) { + return createIndex(s_waylandInternalId -1, 0, s_waylandInternalId); + } + 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::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_waylandInternalId: + return i18n("Internal Windows"); + default: + return QVariant(); + } + } + if (index.internalId() & s_propertyBitMask) { + if (index.column() >= 2 || role != Qt::DisplayRole) { + return QVariant(); + } + if (ShellClient *c = shellClient(index)) { + return propertyData(c, index, role); + } else if (ShellClient *c = internalClient(index)) { + return propertyData(c, index, role); + } else if (Client *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_shellClients); + case s_waylandInternalId: + 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); +} + +ShellClient *DebugConsoleModel::shellClient(const QModelIndex &index) const +{ + return clientForIndex(index, m_shellClients, s_waylandClientId); +} + +ShellClient *DebugConsoleModel::internalClient(const QModelIndex &index) const +{ + return clientForIndex(index, m_internalClients, s_waylandInternalId); +} + +Client *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 KWayland::Server; + + 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); + } + for (auto c : workspace()->desktopList()) { + if (!c->surface()) { + continue; + } + connect(c->surface(), &SurfaceInterface::subSurfaceTreeChanged, this, reset); + } + if (waylandServer()) { + for (auto c : waylandServer()->internalClients()) { + connect(c->surface(), &SurfaceInterface::subSurfaceTreeChanged, this, reset); + } + connect(waylandServer(), &WaylandServer::shellClientAdded, this, + [this, reset] (ShellClient *c) { + connect(c->surface(), &SurfaceInterface::subSurfaceTreeChanged, this, reset); + 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 KWayland::Server; + if (SurfaceInterface *surface = static_cast(parent.internalPointer())) { + const auto &children = surface->childSubSurfaces(); + return children.count(); + } + return 0; + } + const int internalClientsCount = waylandServer() ? waylandServer()->internalClients().count() : 0; + // toplevel are all windows + return workspace()->allClientList().count() + + workspace()->desktopList().count() + + workspace()->unmanagedList().count() + + internalClientsCount; +} + +QModelIndex SurfaceTreeModel::index(int row, int column, const QModelIndex &parent) const +{ + if (column != 0) { + // invalid column + return QModelIndex(); + } + + if (parent.isValid()) { + using namespace KWayland::Server; + 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 &desktopClients = workspace()->desktopList(); + if (row < reference + desktopClients.count()) { + return createIndex(row, column, desktopClients.at(row-reference)->surface()); + } + reference += desktopClients.count(); + const auto &unmanaged = workspace()->unmanagedList(); + if (row < reference + unmanaged.count()) { + return createIndex(row, column, unmanaged.at(row-reference)->surface()); + } + reference += unmanaged.count(); + if (waylandServer()) { + const auto &internal = waylandServer()->internalClients(); + if (row < reference + internal.count()) { + return createIndex(row, column, internal.at(row-reference)->surface()); + } + } + // not found + return QModelIndex(); +} + +QModelIndex SurfaceTreeModel::parent(const QModelIndex &child) const +{ + using namespace KWayland::Server; + 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 &desktopClients = workspace()->desktopList(); + for (int i = 0; i < desktopClients.count(); i++) { + if (desktopClients.at(i)->surface() == parent) { + return createIndex(row + i, 0, parent); + } + } + row += desktopClients.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(); + if (waylandServer()) { + const auto &internal = waylandServer()->internalClients(); + for (int i = 0; i < internal.count(); i++) { + if (internal.at(i)->surface() == parent) { + return createIndex(row + i, 0, parent); + } + } + } + } + return QModelIndex(); +} + +QVariant SurfaceTreeModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + using namespace KWayland::Server; + 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..6cda3ff --- /dev/null +++ b/debug_console.h @@ -0,0 +1,184 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 Client; +class ShellClient; +class Unmanaged; +class DebugConsoleFilter; + +class KWIN_EXPORT DebugConsoleModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit DebugConsoleModel(QObject *parent = nullptr); + virtual ~DebugConsoleModel(); + + + 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: + 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); + ShellClient *shellClient(const QModelIndex &index) const; + ShellClient *internalClient(const QModelIndex &index) const; + Client *x11Client(const QModelIndex &index) const; + Unmanaged *unmanaged(const QModelIndex &index) const; + int topLevelRowCount() const; + + QVector m_shellClients; + QVector m_internalClients; + QVector m_x11Clients; + QVector m_unmanageds; + +}; + +class DebugConsoleDelegate : public QStyledItemDelegate +{ + Q_OBJECT +public: + explicit DebugConsoleDelegate(QObject *parent = nullptr); + virtual ~DebugConsoleDelegate(); + + QString displayText(const QVariant &value, const QLocale &locale) const override; +}; + +class KWIN_EXPORT DebugConsole : public QWidget +{ + Q_OBJECT +public: + DebugConsole(); + virtual ~DebugConsole(); + +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); + virtual ~SurfaceTreeModel(); + + 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); + virtual ~DebugConsoleFilter(); + + void pointerEvent(MouseEvent *event) override; + void wheelEvent(WheelEvent *event) override; + void keyEvent(KeyEvent *event) override; + void touchDown(quint32 id, const QPointF &pos, quint32 time) override; + void touchMotion(quint32 id, const QPointF &pos, quint32 time) override; + void touchUp(quint32 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; + +private: + QTextEdit *m_textEdit; +}; + +namespace LibInput +{ +class Device; +} + +class InputDeviceModel : public QAbstractItemModel +{ + Q_OBJECT +public: + explicit InputDeviceModel(QObject *parent = nullptr); + virtual ~InputDeviceModel(); + + 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..2664c44 --- /dev/null +++ b/decorations/decoratedclient.cpp @@ -0,0 +1,336 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "decoratedclient.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::geometryChanged, 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()); + } + } + ); + 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); + m_compositorToggledConnection = connect(Compositor::self(), &Compositor::compositingToggled, this, + [this, decoration]() { + delete m_renderer; + m_renderer = nullptr; + 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(Cursor::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) +{ + 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(Cursor::pos(), Cursor::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(); +} + +bool DecoratedClientImpl::isMaximizedVertically() const +{ + return m_client->maximizeMode() & MaximizeVertical; +} + +bool DecoratedClientImpl::isMaximized() const +{ + return isMaximizedHorizontally() && isMaximizedVertically(); +} + +bool DecoratedClientImpl::isMaximizedHorizontally() const +{ + return m_client->maximizeMode() & 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..f215032 --- /dev/null +++ b/decorations/decoratedclient.h @@ -0,0 +1,124 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~DecoratedClientImpl(); + 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; + 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); + + AbstractClient *client() { + return m_client; + } + Renderer *renderer() { + return m_renderer; + } + void destroyRenderer(); + KDecoration2::DecoratedClient *decoratedClient() { + return KDecoration2::DecoratedClientPrivate::client(); + } + + void signalShadeChange(); + +private Q_SLOTS: + void delayedRequestToggleMaximization(Options::WindowOperation operation); + +private: + void createRenderer(); + 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..5afdbdf --- /dev/null +++ b/decorations/decorationbridge.cpp @@ -0,0 +1,301 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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_settings() + , m_noPlugin(false) +{ +} + +DecorationBridge::~DecorationBridge() +{ + s_self = nullptr; +} + +static QString readPlugin() +{ + return kwinApp()->config()->group(s_pluginName).readEntry("library", s_defaultPlugin); +} + +static bool readNoPlugin() +{ + return kwinApp()->config()->group(s_pluginName).readEntry("NoPlugin", false); +} + +QString DecorationBridge::readTheme() const +{ + return kwinApp()->config()->group(s_pluginName).readEntry("theme", m_defaultTheme); +} + +void DecorationBridge::init() +{ + using namespace KWayland::Server; + 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() +{ + 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_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(); + } + findTheme(decoSettingsMap); +} + +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; + b.append(QStringLiteral("Plugin: %1\n").arg(m_plugin)); + b.append(QStringLiteral("Theme: %1\n").arg(m_theme)); + 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..e729528 --- /dev/null +++ b/decorations/decorationbridge.h @@ -0,0 +1,87 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_DECORATION_BRIDGE_H +#define KWIN_DECORATION_BRIDGE_H + +#include + +#include + +#include +#include + +class KPluginFactory; + +namespace KDecoration2 +{ +class DecorationSettings; +} + +namespace KWin +{ + +class AbstractClient; + +namespace Decoration +{ + +class DecorationBridge : public KDecoration2::DecorationBridge +{ + Q_OBJECT +public: + virtual ~DecorationBridge(); + + 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; + } + + void reconfigure(); + + const QSharedPointer &settings() const { + return m_settings; + } + + QString supportInformation() const; + +private: + void loadMetaData(const QJsonObject &object); + void findTheme(const QVariantMap &map); + void initPlugin(); + QString readTheme() const; + KPluginFactory *m_factory; + bool m_blur; + 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..e37eeaf --- /dev/null +++ b/decorations/decorationpalette.cpp @@ -0,0 +1,138 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2014 Martin Gräßlin +Copyright 2014 Hugo Pereira Da Costa +Copyright 2015 Mika Allan Rauhala + +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, see . +*********************************************************************/ + +#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.isEmpty() && 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::Background)); + 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.dark()); + + 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..f652c4e --- /dev/null +++ b/decorations/decorationpalette.h @@ -0,0 +1,70 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2014 Martin Gräßlin +Copyright 2014 Hugo Pereira Da Costa +Copyright 2015 Mika Allan Rauhala + +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, see . +*********************************************************************/ + +#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..3efbceb --- /dev/null +++ b/decorations/decorationrenderer.cpp @@ -0,0 +1,88 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "decorationrenderer.h" +#include "decoratedclient.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]{ + m_imageSizesDirty = true; + }; + connect(client->client(), &AbstractClient::screenScaleChanged, this, markImageSizesDirty); + connect(client->decoration(), &KDecoration2::Decoration::bordersChanged, this, markImageSizesDirty); + connect(client->decoratedClient(), &KDecoration2::DecoratedClient::widthChanged, this, markImageSizesDirty); + connect(client->decoratedClient(), &KDecoration2::DecoratedClient::heightChanged, 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(); + QImage image(geo.width() * dpr, geo.height() * dpr, QImage::Format_ARGB32_Premultiplied); + 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); + client()->decoration()->paint(&p, geo); + return image; +} + +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..417bf84 --- /dev/null +++ b/decorations/decorationrenderer.h @@ -0,0 +1,86 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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: + virtual ~Renderer(); + + 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); + +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..15cafda --- /dev/null +++ b/decorations/decorations_logging.cpp @@ -0,0 +1,21 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..3210b04 --- /dev/null +++ b/decorations/decorations_logging.h @@ -0,0 +1,26 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..8c53323 --- /dev/null +++ b/decorations/settings.cpp @@ -0,0 +1,191 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "settings.h" +// KWin +#include "composite.h" +#include "virtualdesktops.h" +#include "workspace.h" +#include "appmenu.h" + +#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, + [this, c] { + disconnect(c); + } + ); + connect(Workspace::self(), &Workspace::configChanged, 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); + } + const auto size = stringToSize(config.readEntry("BorderSize", QStringLiteral("Normal"))); + if (size != m_borderSize) { + m_borderSize = size; + emit decorationSettings()->borderSizeChanged(m_borderSize); + } + + emit decorationSettings()->reconfigured(); +} + +} +} diff --git a/decorations/settings.h b/decorations/settings.h new file mode 100644 index 0000000..632fe36 --- /dev/null +++ b/decorations/settings.h @@ -0,0 +1,66 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~SettingsImpl(); + 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; + } + +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_closeDoubleClickMenu = false; +}; +} // Decoration +} // KWin + +#endif diff --git a/deleted.cpp b/deleted.cpp new file mode 100644 index 0000000..bfb253a --- /dev/null +++ b/deleted.cpp @@ -0,0 +1,211 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak + +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, see . +*********************************************************************/ + +#include "deleted.h" + +#include "workspace.h" +#include "client.h" +#include "netinfo.h" +#include "shadow.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_wasCurrentTab(true) + , m_decorationRenderer(nullptr) + , m_fullscreen(false) + , m_keepAbove(false) + , m_keepBelow(false) +{ +} + +Deleted::~Deleted() +{ + if (delete_refcount != 0) + qCCritical(KWIN_CORE) << "Deleted client has non-zero reference count (" << delete_refcount << ")"; + assert(delete_refcount == 0); + if (workspace()) { + workspace()->removeDeleted(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) +{ + assert(dynamic_cast< Deleted* >(c) == NULL); + Toplevel::copyToDeleted(c); + desk = c->desktop(); + 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(true); + 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) { + connect(c, &AbstractClient::windowClosed, this, &Deleted::mainClientClosed); + } + m_fullscreen = client->isFullScreen(); + m_wasCurrentTab = client->isCurrentTab(); + m_keepAbove = client->keepAbove(); + m_keepBelow = client->keepBelow(); + m_caption = client->caption(); + } +} + +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(); +} + +int Deleted::desktop() const +{ + return desk; +} + +QStringList Deleted::activities() const +{ + return activityList; +} + +QPoint Deleted::clientPos() const +{ + return contentsRect.topLeft(); +} + +QSize Deleted::clientSize() const +{ + return contentsRect.size(); +} + +void Deleted::debug(QDebug& stream) const +{ + stream << "\'ID:" << window() << "\' (deleted)"; +} + +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::decorationRect() const +{ + return rect(); +} + +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); +} + +xcb_window_t Deleted::frameId() const +{ + return m_frame; +} + +double Deleted::opacity() const +{ + return m_opacity; +} + +QByteArray Deleted::windowRole() const +{ + return m_windowRole; +} + +} // namespace + diff --git a/deleted.h b/deleted.h new file mode 100644 index 0000000..868893f --- /dev/null +++ b/deleted.h @@ -0,0 +1,154 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak + +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, see . +*********************************************************************/ + +#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 + Q_PROPERTY(bool minimized READ isMinimized) + Q_PROPERTY(bool modal READ isModal) + Q_PROPERTY(bool fullScreen READ isFullScreen CONSTANT) + Q_PROPERTY(bool isCurrentTab READ isCurrentTab) + Q_PROPERTY(bool keepAbove READ keepAbove CONSTANT) + Q_PROPERTY(bool keepBelow READ keepBelow CONSTANT) + Q_PROPERTY(QString caption READ caption CONSTANT) +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(); + virtual int desktop() const; + virtual QStringList activities() const; + virtual QPoint clientPos() const; + virtual QSize clientSize() const; + QPoint clientContentPos() const override { + return m_contentPos; + } + virtual QRect transparentRect() const; + virtual bool isDeleted() const; + virtual xcb_window_t frameId() const override; + bool noBorder() const { + return no_border; + } + void layoutDecorationRects(QRect &left, QRect &top, QRect &right, QRect &bottom) const; + QRect decorationRect() const; + virtual Layer layer() const { + 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; + 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 isCurrentTab() const { + return m_wasCurrentTab; + } + bool keepAbove() const { + return m_keepAbove; + } + bool keepBelow() const { + return m_keepBelow; + } + QString caption() const { + return m_caption; + } +protected: + virtual void debug(QDebug& stream) const; +private Q_SLOTS: + void mainClientClosed(KWin::Toplevel *client); +private: + Deleted(); // use create() + void copyToDeleted(Toplevel* c); + virtual ~Deleted(); // deleted only using unrefWindow() + int delete_refcount; + double window_opacity; + int desk; + QStringList activityList; + QRect contentsRect; // for clientPos()/clientSize() + QPoint m_contentPos; + QRect transparent_rect; + xcb_window_t m_frame; + + 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; + bool m_wasCurrentTab; + 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; +}; + +inline void Deleted::refWindow() +{ + ++delete_refcount; +} + +} // namespace + +Q_DECLARE_METATYPE(KWin::Deleted*) + +#endif 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/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 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..4b13fb6e2a1f45622f1eff96affb3c35198e08a8 GIT binary patch literal 483 zcmV<90UZ8`P)UTnz$I$kRZg+SS^z+ZqErq5tBYaTNBQ9Z_c~@ za!=k%1=n@i_028b9S1>(Qi9Em41|lz2GdtGsEQH@i1qS{=Uxwz<2q1MDKN$Y0a4Tx z9$PJlfqZ?6LIWZJ2rQOyg=Qn60a-*dDT&8?d~^uYG<|tBO~cOa9v_I`XiU>Yx7+nS zoG0@C8O>56frDxlwr!7ks=ADLJod$F7zRIx-xZ32h<5u4r^d;sw_mBC)9HA8`TQ0{ zLF9^tYa`z&olbjdN-6h@xqu86iAc!haz8pd7m#5zu~>XI5dT>+V8Y87P$+CemSsev z(Xm!~E=z5W!NW70`B+1<&`|Lx4~h}C+drqdZ; zQG`ybR3ta8CgsUwMzeKI-L|y?tUlK)mLHU&IKr;gP)cbXcD+U%$CR{QJHw0vlmV1d z#sG|Gz&Jor0^oIh_Op#;$6d+e$2S$Ft!1YLyB;8Mvgn{cZ z@&nWX4fuTn=b8|LeyYf6{lbgJ7B>oNNf-)st^*Wh(HQSu@fA79*Z=h#E}@M zk?aLOX*QmDH9;#M-Zd|*Hdj8_2T)23S)n+pnkj|H{eA?f~ z3&MbAqk)Lz0@7S<@NRDpoy*&2s4yTNkXu@gt1ylf2Ba{l@mv?S4-M@@m1*-hqNxu2Q@7)ff}TC38mEJHYeApCpYtvrAXr)KO(`uW+iV#z8b)`&zXYrvHM z#5kTv_v%ZIzH&-YlwcDD)}YYHFh+SAv>C@ZDnpHc#OaNH#X@W~Sg=e004s=f=E&fP zo$biu-61~$i&zC>P@=&qK*ljyKwR?J@5dy)2moaaYH3^tpQ92ojIc?9(w5`(-|uTg z+T7k+5+lWaswDAz;p?XRUB^ zWx)0wL&+?rY0l-y`soqdcMWB8awjIr*2Ru=ea$QkV4}j@M+$R8D-6j0)J&@|AfE46 z=yto*>-Ag!z8xIk`xR8D(~&oCcNq+Zxq$eU3hP%c^Bb{~C2L0*VUYj;002ovPDHLk FV1g?=6T<)i literal 0 HcmV?d00001 diff --git a/doc/kwineffects/index.docbook b/doc/kwineffects/index.docbook new file mode 100644 index 0000000..3cef55a --- /dev/null +++ b/doc/kwineffects/index.docbook @@ -0,0 +1,86 @@ + + + +]> + +
+ + +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, Candy +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..ac8e8c0349b7f4f82c6e73a10da45aabc22dd048 GIT binary patch literal 400 zcmV;B0dM|^P)1R9J=WmceSmKomu9Bos2y7D6Jl zrb3GpKgFM^mVQWW^=GsdNPAV zPn^XQSL$fCSwpEydaM9&blbrC?hy;S^{GsyD%W+$GwQb)7RW_xR2{j*K6lO$LB=KmuRNduBc6PGj~Dq|Wv&x2tYLIA#J uGccw>ab1_cy? + + +]> + +
+ + +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..99f9334 --- /dev/null +++ b/doc/windowbehaviour/index.docbook @@ -0,0 +1,686 @@ + + + +]> + + + +
+ +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 tabbing and 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 &Alt;) 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. + + + + + + +Window Tabbing + + +Automatically group similar windows +When turned on attempt to automatically detect when a newly opened window is +related to an existing one and place them in the same window group. + + + +Switch to automatically grouped windows immediately +When turned on immediately switch to any new window tabs that were +automatically added to the current group. + + + +Placement +The placement policy determines where a new window will appear +on the desktop. Smart will try to achieve a minimum +overlap of windows, Cascade will cascade the +windows, and Random will use a random +position. Centered will open all new windows in +the center of the screen, and Zero-Cornered 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 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

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. +Smart - place where no other window exists. +Maximizing - start the window maximized. +Cascade - staircase-by-title. +Centered - center of the desktop. +Random +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 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

};Qy)|~Jaozs$Ee2{pZfaq2$pY^s0_5)1- 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 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 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

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 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 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 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 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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 + +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, see . +*********************************************************************/ +// 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..7dfee16 --- /dev/null +++ b/effectloader.h @@ -0,0 +1,379 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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: + virtual ~AbstractEffectLoader(); + + /** + * @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 effecName 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..cc3232a --- /dev/null +++ b/effects.cpp @@ -0,0 +1,2069 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2010, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ + +#include "effects.h" + +#include "effectsadaptor.h" +#include "effectloader.h" +#ifdef KWIN_BUILD_ACTIVITIES +#include "activities.h" +#endif +#include "deleted.h" +#include "client.h" +#include "cursor.h" +#include "group.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 +#include + +#include + +#include +#include "composite.h" +#include "xcbutils.h" +#include "platform.h" +#include "shell_client.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()), + NULL)); + 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(NULL) + , fullscreen_effect(0) + , 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() : 0); + // 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](Client *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::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(vds, &VirtualDesktopManager::countChanged, this, &EffectsHandler::numberDesktopsChanged); + connect(Cursor::self(), &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(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 (Client *c : ws->clientList()) { + setupClientConnections(c); + } + for (Unmanaged *u : ws->unmanagedList()) { + setupUnmanagedConnections(u); + } + if (auto w = waylandServer()) { + connect(w, &WaylandServer::shellClientAdded, this, + [this](ShellClient *c) { + if (c->readyForPainting()) + slotShellClientShown(c); + else + connect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotShellClientShown); + } + ); + } + reconfigure(); +} + +EffectsHandlerImpl::~EffectsHandlerImpl() +{ + unloadAllEffects(); +} + +void EffectsHandlerImpl::unloadAllEffects() +{ + makeOpenGLContextCurrent(); + if (keyboard_grab_effect != NULL) + ungrabKeyboard(); + setActiveFullScreenEffect(nullptr); + for (auto it = loaded_effects.begin(); it != loaded_effects.end(); ++it) { + Effect *effect = (*it).second; + stopMouseInterception(effect); + // remove support properties for the effect + const QList properties = m_propertiesForEffects.keys(); + for (const QByteArray &property : properties) { + removeSupportProperty(property, effect); + } + delete effect; + } + loaded_effects.clear(); + m_effectLoader->clear(); +} + +void EffectsHandlerImpl::setupAbstractClientConnections(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::damaged, this, &EffectsHandlerImpl::slotWindowDamaged); + 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()); + } + ); +} + +void EffectsHandlerImpl::setupClientConnections(Client* c) +{ + setupAbstractClientConnections(c); + connect(c, &Client::paddingChanged, this, &EffectsHandlerImpl::slotPaddingChanged); +} + +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::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, QRegion region, 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, QRegion region, 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, QRegion region, 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 NULL; +} + +void EffectsHandlerImpl::drawWindow(EffectWindow* w, int mask, QRegion region, 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(dynamic_cast(t)); + Client *c = static_cast(t); + disconnect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotClientShown); + setupClientConnections(c); + if (!c->tabGroup()) // the "window" has already been there + emit windowAdded(c->effectWindow()); +} + +void EffectsHandlerImpl::slotShellClientShown(Toplevel *t) +{ + ShellClient *c = static_cast(t); + setupAbstractClientConnections(c); + emit windowAdded(t->effectWindow()); +} + +void EffectsHandlerImpl::slotUnmanagedShown(KWin::Toplevel *t) +{ // regardless, unmanaged windows are -yet?- not synced anyway + Q_ASSERT(dynamic_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 == NULL || t->effectWindow() == NULL) + return; + emit windowGeometryShapeChanged(t->effectWindow(), old); +} + +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 == NULL || t->effectWindow() == NULL) + return; + emit windowPaddingChanged(t->effectWindow(), old); +} + +void EffectsHandlerImpl::setActiveFullScreenEffect(Effect* e) +{ + if (fullscreen_effect == e) { + return; + } + fullscreen_effect = e; + emit activeFullScreenEffectChanged(); +} + +Effect* EffectsHandlerImpl::activeFullScreenEffect() const +{ + return fullscreen_effect; +} + +bool EffectsHandlerImpl::grabKeyboard(Effect* effect) +{ + if (keyboard_grab_effect != NULL) + return false; + if (!doGrabKeyboard()) { + return false; + } + keyboard_grab_effect = effect; + return true; +} + +bool EffectsHandlerImpl::doGrabKeyboard() +{ + return true; +} + +void EffectsHandlerImpl::ungrabKeyboard() +{ + assert(keyboard_grab_effect != NULL); + doUngrabKeyboard(); + keyboard_grab_effect = NULL; +} + +void EffectsHandlerImpl::doUngrabKeyboard() +{ +} + +void EffectsHandlerImpl::grabbedKeyboardEvent(QKeyEvent* e) +{ + if (keyboard_grab_effect != NULL) + 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(quint32 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(quint32 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(quint32 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 NULL; +} + +void EffectsHandlerImpl::startMousePolling() +{ + if (Cursor::self()) + Cursor::self()->startMousePolling(); +} + +void EffectsHandlerImpl::stopMousePolling() +{ + if (Cursor::self()) + Cursor::self()->stopMousePolling(); +} + +bool EffectsHandlerImpl::hasKeyboardGrab() const +{ + return keyboard_grab_effect != NULL; +} + +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 (AbstractClient* cl = dynamic_cast< AbstractClient* >(static_cast(c)->window())) + Workspace::self()->activateClient(cl, true); +} + +EffectWindow* EffectsHandlerImpl::activeWindow() const +{ + return Workspace::self()->activeClient() ? Workspace::self()->activeClient()->effectWindow() : NULL; +} + +void EffectsHandlerImpl::moveWindow(EffectWindow* w, const QPoint& pos, bool snap, double snapAdjust) +{ + AbstractClient* cl = dynamic_cast< AbstractClient* >(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) +{ + AbstractClient* cl = dynamic_cast< AbstractClient* >(static_cast(w)->window()); + if (cl && !cl->isDesktop() && !cl->isDock()) + Workspace::self()->sendClientToDesktop(cl, desktop, true); +} + +void EffectsHandlerImpl::windowToScreen(EffectWindow* w, int screen) +{ + AbstractClient* cl = dynamic_cast< AbstractClient* >(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 (Client* w = Workspace::self()->findClient(Predicate::WindowMatch, id)) + return w->effectWindow(); + if (Unmanaged* w = Workspace::self()->findUnmanaged(id)) + return w->effectWindow(); + if (waylandServer()) { + if (ShellClient *w = waylandServer()->findClient(id)) { + return w->effectWindow(); + } + } + return NULL; +} + +EffectWindow* EffectsHandlerImpl::findWindow(KWayland::Server::SurfaceInterface *surf) const +{ + if (waylandServer()) { + if (ShellClient *w = waylandServer()->findClient(surf)) { + return w->effectWindow(); + } + } + return nullptr; +} + + +EffectWindowList EffectsHandlerImpl::stackingOrder() const +{ + ToplevelList 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 (AbstractClient* c = dynamic_cast< AbstractClient* >(static_cast< EffectWindowImpl* >(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(); +#endif + return QList< int >(); +} + +int EffectsHandlerImpl::currentTabBoxDesktop() const +{ +#ifdef KWIN_BUILD_TABBOX + return TabBox::TabBox::self()->currentDesktop(); +#endif + return -1; +} + +EffectWindow* EffectsHandlerImpl::currentTabBoxWindow() const +{ +#ifdef KWIN_BUILD_TABBOX + if (auto c = TabBox::TabBox::self()->currentClient()) + return c->effectWindow(); +#endif + return NULL; +} + +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 AbstractClient* cl = dynamic_cast< const AbstractClient* >(t)) + return Workspace::self()->clientArea(opt, cl); + else + return Workspace::self()->clientArea(opt, t->geometry().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(Cursor::self(), &Cursor::cursorChanged, this, &EffectsHandler::cursorShapeChanged); + Cursor::self()->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) { + Cursor::self()->stopCursorTracking(); + disconnect(Cursor::self(), &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 Cursor::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) +{ + makeOpenGLContextCurrent(); + m_compositor->addRepaintFull(); + + for (QMap< int, EffectPair >::iterator it = effect_order.begin(); it != effect_order.end(); ++it) { + if (it.value().first == name) { + qCDebug(KWIN_CORE) << "EffectsHandler::unloadEffect : Unloading Effect : " << name; + if (activeFullScreenEffect() == it.value().second) { + setActiveFullScreenEffect(0); + } + stopMouseInterception(it.value().second); + // remove support properties for the effect + const QList properties = m_propertiesForEffects.keys(); + for (const QByteArray &property : properties) { + removeSupportProperty(property, it.value().second); + } + delete it.value().second; + effect_order.erase(it); + effectsChanged(); + return; + } + } + + qCDebug(KWIN_CORE) << "EffectsHandler::unloadEffect : Effect not loaded : " << name; +} + +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; +} + +KWayland::Server::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 kwinApp()->inputConfig(); +} + +//**************************************** +// EffectWindowImpl +//**************************************** + +EffectWindowImpl::EffectWindowImpl(Toplevel *toplevel) + : EffectWindow(toplevel) + , toplevel(toplevel) + , sw(NULL) +{ +} + +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); +} + +const EffectWindowGroup* EffectWindowImpl::group() const +{ + if (Client* c = dynamic_cast< Client* >(toplevel)) + return c->group()->effectGroup(); + return NULL; // TODO +} + +void EffectWindowImpl::refWindow() +{ + if (Deleted* d = dynamic_cast< Deleted* >(toplevel)) + return d->refWindow(); + abort(); // TODO +} + +void EffectWindowImpl::unrefWindow() +{ + if (Deleted* d = dynamic_cast< Deleted* >(toplevel)) + return d->unrefWindow(); // delays deletion in case + abort(); // TODO +} + +void EffectWindowImpl::setWindow(Toplevel* w) +{ + toplevel = w; + setParent(w); +} + +void EffectWindowImpl::setSceneWindow(Scene::Window* w) +{ + sw = w; +} + +QRegion EffectWindowImpl::shape() const +{ + return sw ? sw->shape() : geometry(); +} + +QRect EffectWindowImpl::decorationInnerRect() const +{ + Client *client = dynamic_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() +{ + if (AbstractClient* c = dynamic_cast< AbstractClient* >(toplevel)) { + if (AbstractClient* c2 = c->findModal()) + return c2->effectWindow(); + } + return NULL; +} + +template +EffectWindowList getMainWindows(Toplevel *toplevel) +{ + T *c = static_cast(toplevel); + 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 (dynamic_cast(toplevel)) { + return getMainWindows(toplevel); + } else if (toplevel->isDeleted()) { + return getMainWindows(toplevel); + } + return EffectWindowList(); +} + +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, SIGNAL(wIdChanged(qulonglong)), SLOT(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, QWeakPointer(static_cast(w))); + } else { + m_thumbnails.insert(item, QWeakPointer()); + } +} + +void EffectWindowImpl::desktopThumbnailDestroyed(QObject *object) +{ + // we know it is a DesktopThumbnailItem + m_desktopThumbnails.removeAll(static_cast(object)); +} + +void EffectWindowImpl::referencePreviousWindowPixmap() +{ + if (sw) { + sw->referencePreviousPixmap(); + } +} + +void EffectWindowImpl::unreferencePreviousWindowPixmap() +{ + if (sw) { + sw->unreferencePreviousPixmap(); + } +} + +//**************************************** +// 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(0) + , EffectFrame() + , m_style(style) + , m_static(staticSize) + , m_point(position) + , m_alignment(alignment) + , m_shader(NULL) + , 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(QRegion region, double opacity, double frameOpacity) +{ + if (m_geometry.isEmpty()) { + return; // Nothing to display + } + m_shader = NULL; + 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..3b68e3d --- /dev/null +++ b/effects.h @@ -0,0 +1,563 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2010, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef KWIN_EFFECTSIMPL_H +#define KWIN_EFFECTSIMPL_H + +#include "kwineffects.h" + +#include "scene.h" + +#include +#include + +#include + +namespace Plasma { +class Theme; +} + +namespace KWayland +{ +namespace Server +{ +class Display; +} +} + +class QDBusPendingCallWatcher; +class QDBusServiceWatcher; + + +namespace KWin +{ + +class AbstractThumbnailItem; +class DesktopThumbnailItem; +class WindowThumbnailItem; + +class AbstractClient; +class Client; +class Compositor; +class Deleted; +class EffectLoader; +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); + virtual ~EffectsHandlerImpl(); + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, QRegion region, 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, QRegion region, WindowPaintData& data) override; + void postPaintWindow(EffectWindow* w) override; + void paintEffectFrame(EffectFrame* frame, QRegion region, double opacity, double frameOpacity) override; + + Effect *provides(Effect::Feature ef); + + void drawWindow(EffectWindow* w, int mask, QRegion region, 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(KWayland::Server::SurfaceInterface *surf) 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; + + 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; + } + + KWayland::Server::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(quint32 id, const QPointF &pos, quint32 time); + bool touchMotion(quint32 id, const QPointF &pos, quint32 time); + bool touchUp(quint32 id, quint32 time); + + void highlightWindows(const QVector &windows); + + bool isPropertyTypeRegistered(xcb_atom_t atom) const { + return registered_atoms.contains(atom); + } + +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 slotShellClientShown(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 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 setupAbstractClientConnections(KWin::AbstractClient *c); + void setupClientConnections(KWin::Client *c); + 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); + 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); + virtual ~EffectWindowImpl(); + + void enablePainting(int reason) override; + void disablePainting(int reason) override; + bool isPaintingEnabled() override; + + void refWindow() override; + void unrefWindow() override; + + const EffectWindowGroup* group() const override; + + QRegion shape() 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; + EffectWindowList mainWindows() const override; + + WindowQuadList buildQuads(bool force = false) const override; + + void referencePreviousWindowPixmap() override; + void unreferencePreviousWindowPixmap() 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); + QVariant data(int role) const; + + 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; +}; + +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); + virtual ~EffectFrameImpl(); + + void free() override; + void render(QRegion region = 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..06445cc --- /dev/null +++ b/effects/CMakeLists.txt @@ -0,0 +1,194 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kwin_effects\" -DEFFECT_BUILTINS) + +include_directories(${KWIN_SOURCE_DIR}) # for xcbutils.h + +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::WindowSystem + KF5::Plasma # screenedge effect + KF5::IconThemes + KF5::Service + KF5::Notifications # screenshot effect +) + +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::XCB + XCB::IMAGE + 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_OWN_LIBS} ${kwin_effect_KDE_LIBS} ${kwin_effect_QT_LIBS} ${kwin_effect_XLIB_LIBS} ${kwin_effect_XCB_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 + logging.cpp + effect_builtins.cpp + blur/blur.cpp + blur/blurshader.cpp + colorpicker/colorpicker.cpp + cube/cube.cpp + cube/cube_proxy.cpp + cube/cubeslide.cpp + coverswitch/coverswitch.cpp + desktopgrid/desktopgrid.cpp + diminactive/diminactive.cpp + flipswitch/flipswitch.cpp + glide/glide.cpp + invert/invert.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 + scale/scale.cpp + showfps/showfps.cpp + slide/slide.cpp + thumbnailaside/thumbnailaside.cpp + touchpoints/touchpoints.cpp + trackmouse/trackmouse.cpp + windowgeometry/windowgeometry.cpp + wobblywindows/wobblywindows.cpp + zoom/zoom.cpp + ) + +qt5_add_resources( kwin4_effect_builtins_sources shaders.qrc ) + +kconfig_add_kcfg_files(kwin4_effect_builtins_sources + blur/blurconfig.kcfgc + cube/cubeslideconfig.kcfgc + cube/cubeconfig.kcfgc + coverswitch/coverswitchconfig.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 + scale/scaleconfig.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 +add_subdirectory( dialogparent ) +add_subdirectory( eyeonscreen ) +add_subdirectory( fade ) +add_subdirectory( fadedesktop ) +add_subdirectory( frozenapp ) +add_subdirectory( login ) +add_subdirectory( logout ) +add_subdirectory( maximize ) +add_subdirectory( morphingpopups ) +add_subdirectory( translucency ) +add_subdirectory( windowaperture ) + +############################################################################### +# Built-in effects go here + +# Common effects +add_subdirectory( desktopgrid ) +add_subdirectory( diminactive ) +include( dimscreen/CMakeLists.txt ) +include( fallapart/CMakeLists.txt ) +include( highlightwindow/CMakeLists.txt ) +include( kscreen/CMakeLists.txt ) +add_subdirectory( magiclamp ) +include( minimizeanimation/CMakeLists.txt ) +add_subdirectory( presentwindows ) +add_subdirectory( resize ) +include( screenedge/CMakeLists.txt ) +add_subdirectory( showfps ) +include( showpaint/CMakeLists.txt ) +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( flipswitch ) +add_subdirectory( glide ) +add_subdirectory( invert ) +add_subdirectory( lookingglass ) +add_subdirectory( magnifier ) +add_subdirectory( mouseclick ) +add_subdirectory( mousemark ) +add_subdirectory( scale ) +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..22814c3 --- /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 $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..44ffb6c --- /dev/null +++ b/effects/backgroundcontrast/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# 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..3705a64 --- /dev/null +++ b/effects/backgroundcontrast/contrast.cpp @@ -0,0 +1,486 @@ +/* + * Copyright © 2010 Fredrik Höglund + * Copyright © 2011 Philipp Knechtges + * Copyright 2014 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#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); + KWayland::Server::Display *display = effects->waylandDisplay(); + if (display) { + m_contrastManager = display->createContrastManager(this); + m_contrastManager->create(); + } + } else { + net_wm_contrast_region = 0; + } + + connect(effects, SIGNAL(windowAdded(KWin::EffectWindow*)), this, SLOT(slotWindowAdded(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowDeleted(KWin::EffectWindow*)), this, SLOT(slotWindowDeleted(KWin::EffectWindow*))); + connect(effects, SIGNAL(propertyNotify(KWin::EffectWindow*,long)), this, SLOT(slotPropertyNotify(KWin::EffectWindow*,long))); + connect(effects, SIGNAL(screenGeometryChanged(QSize)), this, SLOT(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; + } + } + + KWayland::Server::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()); + } + + //!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) +{ + KWayland::Server::SurfaceInterface *surf = w->surface(); + + if (surf) { + m_contrastChangedConnections[w] = connect(surf, &KWayland::Server::SurfaceInterface::contrastChanged, this, [this, w] () { + + if (w) { + updateContrastRegion(w); + } + }); + } + updateContrastRegion(w); +} + +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, QRegion region, 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(); + QVector shapeRects = shape.rects(); + shape = QRegion(); // clear + foreach (QRect r, shapeRects) { + 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()); + shape |= r; + } + shape = shape & 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, QRegion region, 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(); + 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(); +} + +} // namespace KWin + diff --git a/effects/backgroundcontrast/contrast.h b/effects/backgroundcontrast/contrast.h new file mode 100644 index 0000000..cdf0609 --- /dev/null +++ b/effects/backgroundcontrast/contrast.h @@ -0,0 +1,104 @@ +/* + * Copyright © 2010 Fredrik Höglund + * Copyright 2014 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef CONTRAST_H +#define CONTRAST_H + +#include +#include +#include + +#include +#include + +namespace KWayland +{ +namespace Server +{ +class ContrastManagerInterface; +} +} + +namespace KWin +{ + +class ContrastShader; + +class ContrastEffect : public KWin::Effect +{ + Q_OBJECT +public: + ContrastEffect(); + ~ContrastEffect(); + + static bool supported(); + static bool enabledByDefault(); + + static QMatrix4x4 colorMatrix(qreal contrast, qreal intensity, qreal saturation); + void reconfigure(ReconfigureFlags flags); + void prePaintScreen(ScreenPrePaintData &data, int time); + void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time); + void drawWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data); + void paintEffectFrame(EffectFrame *frame, QRegion region, double opacity, double frameOpacity); + + virtual bool provides(Feature feature); + + int requestedEffectChainPosition() const override { + return 76; + } + +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 + KWayland::Server::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..64a7087 --- /dev/null +++ b/effects/backgroundcontrast/contrastshader.cpp @@ -0,0 +1,210 @@ +/* + * Copyright © 2010 Fredrik Höglund + * Copyright 2014 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "contrastshader.h" + +#include +#include + +#include +#include +#include +#include + +#include + +using namespace KWin; + + +ContrastShader::ContrastShader() + : mValid(false), shader(NULL), m_opacity(1) +{ +} + +ContrastShader::~ContrastShader() +{ + reset(); +} + +ContrastShader *ContrastShader::create() +{ + return new ContrastShader(); +} + +void ContrastShader::reset() +{ + delete shader; + shader = NULL; + + 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()); +} + diff --git a/effects/backgroundcontrast/contrastshader.h b/effects/backgroundcontrast/contrastshader.h new file mode 100644 index 0000000..61d8df7 --- /dev/null +++ b/effects/backgroundcontrast/contrastshader.h @@ -0,0 +1,76 @@ +/* + * Copyright © 2010 Fredrik Höglund + * Copyright 2014 Marco Martin + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; see the file COPYING. if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#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..b4c99d3 --- /dev/null +++ b/effects/blur/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_blur_config_SRCS blur_config.cpp) +ki18n_wrap_ui(kwin_blur_config_SRCS blur_config.ui) +qt5_add_dbus_interface(kwin_blur_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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 + Qt5::DBus + KF5::ConfigWidgets + KF5::I18n + KF5::Service +) + +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..be4d4fa --- /dev/null +++ b/effects/blur/blur.cpp @@ -0,0 +1,771 @@ +/* + * Copyright © 2010 Fredrik Höglund + * Copyright © 2011 Philipp Knechtges + * Copyright © 2018 Alex Nemeth + * + * 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; see the file COPYING. if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "blur.h" +#include "blurshader.h" +// KConfigSkeleton +#include "blurconfig.h" + +#include +#include +#include +#include // for QGuiApplication +#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); + KWayland::Server::Display *display = effects->waylandDisplay(); + if (display) { + m_blurManager = display->createBlurManager(this); + m_blurManager->create(); + } + } else { + net_wm_blur_region = 0; + } + + connect(effects, SIGNAL(windowAdded(KWin::EffectWindow*)), this, SLOT(slotWindowAdded(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowDeleted(KWin::EffectWindow*)), this, SLOT(slotWindowDeleted(KWin::EffectWindow*))); + connect(effects, SIGNAL(propertyNotify(KWin::EffectWindow*,long)), this, SLOT(slotPropertyNotify(KWin::EffectWindow*,long))); + connect(effects, SIGNAL(screenGeometryChanged(QSize)), this, SLOT(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); + + for (int i = 0; i <= m_downSampleIterations; i++) { + m_renderTextures.append(GLTexture(GL_RGBA8, 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(GL_RGBA8, 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); + } + } + } + + KWayland::Server::SurfaceInterface *surf = w->surface(); + + if (surf && surf->blur()) { + region = surf->blur()->region(); + } + + //!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) +{ + KWayland::Server::SurfaceInterface *surf = w->surface(); + + if (surf) { + windowBlurChangedConnections[w] = connect(surf, &KWayland::Server::SurfaceInterface::blurChanged, this, [this, w] () { + if (w) { + updateBlurRegion(w); + } + }); + } + + 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::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(); + 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(); + } + } 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(); + 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_damagedArea = QRegion(); + 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; + + const QRegion oldPaint = data.paint; + + // 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 keep track of the "damage propagation" + m_damagedArea |= (w->isDock() ? (expandedBlur & m_damagedArea) : expand(expandedBlur & m_damagedArea)) & blurArea; + // 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; + + // we don't consider damaged areas which are occluded and are not + // explicitly damaged by this window + m_damagedArea -= data.clip; + m_damagedArea |= oldPaint; + + // in contrast to m_damagedArea does m_paintedArea keep track of all repainted areas + 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, QRegion region, 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(); + QVector shapeRects = shape.rects(); + shape = QRegion(); // clear + foreach (QRect r, shapeRects) { + 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()); + shape |= r; + } + shape = shape & region; + + //Only translated, not scaled + } else if (translated) { + shape = shape.translated(data.xTranslation(), data.yTranslation()); + shape = shape & region; + } + + if (!shape.isEmpty()) { + doBlur(shape, screen, data.opacity(), data.screenProjectionMatrix(), w->isDock(), w->geometry()); + } + } + + // Draw the window over the blurred area + effects->drawWindow(w, mask, region, data); +} + +void BlurEffect::paintEffectFrame(EffectFrame *frame, QRegion region, 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); + + // Upload geometry for the down and upsample iterations + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + + 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); + copyScreenSampleTexture(vbo, blurRectCount, shape.translated(xTranslate, yTranslate), screenProjection); + } else { + m_renderTargets.first()->blitFromFramebuffer(sourceRect, destRect); + + // 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 (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(); +} + +} // namespace KWin + diff --git a/effects/blur/blur.h b/effects/blur/blur.h new file mode 100644 index 0000000..ec1d845 --- /dev/null +++ b/effects/blur/blur.h @@ -0,0 +1,148 @@ +/* + * Copyright © 2010 Fredrik Höglund + * Copyright © 2018 Alex Nemeth + * + * 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; see the file COPYING. if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#ifndef BLUR_H +#define BLUR_H + +#include +#include +#include + +#include +#include +#include + +namespace KWayland +{ +namespace Server +{ +class BlurManagerInterface; +} +} + +namespace KWin +{ + +static const int borderSize = 5; + +class BlurShader; + +class BlurEffect : public KWin::Effect +{ + Q_OBJECT + +public: + BlurEffect(); + ~BlurEffect(); + + static bool supported(); + static bool enabledByDefault(); + + void reconfigure(ReconfigureFlags flags); + void prePaintScreen(ScreenPrePaintData &data, int time); + void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time); + void drawWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data); + void paintEffectFrame(EffectFrame *frame, QRegion region, double opacity, double frameOpacity); + + virtual bool provides(Feature feature); + + int requestedEffectChainPosition() const override { + return 75; + } + +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_damagedArea; // keeps track of the area which has been damaged (from bottom to top) + QRegion m_paintedArea; // actually painted area which is greater than m_damagedArea + 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; + KWayland::Server::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..a39882a --- /dev/null +++ b/effects/blur/blur_config.cpp @@ -0,0 +1,62 @@ +/* + * Copyright © 2010 Fredrik Höglund + * + * 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; see the file COPYING. if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#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..05db7b1 --- /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[be]=Blur +Name[bg]=Замъгляване +Name[bn]=ব্লার +Name[bn_IN]=Blur (ব্লার) +Name[bs]=Zamućenje +Name[ca]=Difuminat +Name[ca@valencia]=Difuminat +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]=Blur +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[tg]=Шуста +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..bc33efc --- /dev/null +++ b/effects/blur/blur_config.h @@ -0,0 +1,46 @@ +/* + * Copyright © 2010 Fredrik Höglund + * + * 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; see the file COPYING. if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#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 = 0, const QVariantList& args = QVariantList()); + ~BlurEffectConfig(); + + void save(); + +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..2495cd4 --- /dev/null +++ b/effects/blur/blurshader.cpp @@ -0,0 +1,445 @@ +/* + * Copyright © 2010 Fredrik Höglund + * Copyright © 2018 Alex Nemeth + * + * 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; see the file COPYING. if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#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..7158e2b --- /dev/null +++ b/effects/blur/blurshader.h @@ -0,0 +1,116 @@ +/* + * Copyright © 2010 Fredrik Höglund + * Copyright © 2018 Alex Nemeth + * + * 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; see the file COPYING. if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#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..ea36231 --- /dev/null +++ b/effects/colorpicker/colorpicker.cpp @@ -0,0 +1,131 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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, QRegion region, 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..f3856ce --- /dev/null +++ b/effects/colorpicker/colorpicker.h @@ -0,0 +1,65 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(); + virtual ~ColorPickerEffect(); + void paintScreen(int mask, QRegion region, 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..c1a6aed --- /dev/null +++ b/effects/coverswitch/CMakeLists.txt @@ -0,0 +1,27 @@ +####################################### +# Effect + +####################################### +# Config +set(kwin_coverswitch_config_SRCS coverswitch_config.cpp) +ki18n_wrap_ui(kwin_coverswitch_config_SRCS coverswitch_config.ui) +qt5_add_dbus_interface(kwin_coverswitch_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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 + Qt5::DBus + KF5::ConfigWidgets + KF5::I18n + KF5::Service +) + +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..463e13e --- /dev/null +++ b/effects/coverswitch/coverswitch.cpp @@ -0,0 +1,1000 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(0) + , captionFrame(NULL) + , 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 = NULL; + } + connect(effects, SIGNAL(windowClosed(KWin::EffectWindow*)), this, SLOT(slotWindowClosed(KWin::EffectWindow*))); + connect(effects, SIGNAL(tabBoxAdded(int)), this, SLOT(slotTabBoxAdded(int))); + connect(effects, SIGNAL(tabBoxClosed()), this, SLOT(slotTabBoxClosed())); + connect(effects, SIGNAL(tabBoxUpdated()), this, SLOT(slotTabBoxUpdated())); + connect(effects, SIGNAL(tabBoxKeyEvent(QKeyEvent*)), this, SLOT(slotTabBoxKeyEvent(QKeyEvent*))); +} + +CoverSwitchEffect::~CoverSwitchEffect() +{ + delete captionFrame; + delete m_reflectionShader; +} + +bool CoverSwitchEffect::supported() +{ + return effects->isOpenGLCompositing() && effects->animationsSupported(); +} + +void CoverSwitchEffect::reconfigure(ReconfigureFlags) +{ + CoverSwitchConfig::self()->read(); + animationDuration = animationTime(200); + animateSwitch = CoverSwitchConfig::animateSwitch(); + animateStart = CoverSwitchConfig::animateStart(); + animateStop = CoverSwitchConfig::animateStop(); + reflection = CoverSwitchConfig::reflection(); + windowTitle = CoverSwitchConfig::windowTitle(); + zPosition = CoverSwitchConfig::zPosition(); + timeLine.setCurveShape(QTimeLine::EaseInOutCurve); + 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.setCurrentTime(timeLine.currentTime() + time); + } + if (selected_window == NULL) + abort(); + } + effects->prePaintScreen(data, time); +} + +void CoverSwitchEffect::paintScreen(int mask, QRegion region, 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.currentValue(); + } else if (stop) { + mirrorColor[0][3] = 1.0 - timeLine.currentValue(); + } 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.currentValue(); + else if (stop) + opacity = 1.0 - timeLine.currentValue(); + if (animation) + captionFrame->setCrossFadeProgress(timeLine.currentValue()); + captionFrame->render(region, opacity); + } + } +} + +void CoverSwitchEffect::postPaintScreen() +{ + if ((mActivated && (animation || start)) || stop || stopRequested) { + if (timeLine.currentValue() == 1.0) { + timeLine.setCurrentTime(0); + if (stop) { + stop = false; + effects->setActiveFullScreenEffect(0); + 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.currentValue() < 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.currentValue() < 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.currentValue()); + if (stop) + data.setOpacity(timeLine.currentValue()); + } else + return; + } + } + if ((start || stop) && (!w->isOnCurrentDesktop() || w->isMinimized())) { + if (stop) // Fade out windows not on the current desktop + data.setOpacity((1.0 - timeLine.currentValue())); + else // Fade in Windows from other desktops when animation is started + data.setOpacity(timeLine.currentValue()); + } + 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.setCurrentTime(timeLine.duration() - timeLine.currentValue()); + } else { + stopRequested = true; + } + } else + effects->setActiveFullScreenEffect(0); + 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 != NULL) { + 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.currentValue()); + } else { + const QVector3D translation = data.translation() * timeLine.currentValue(); + 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.currentValue())); + } + if (clientRect.height() != fullRect.height() && clientRect.y() != fullRect.y()) { + data.translate(0.0, - clientRect.y() * (1.0f - timeLine.currentValue())); + } + } else { + if (clientRect.width() != fullRect.width() && clientRect.x() < area.x()) { + data.translate(- clientRect.width() * (1.0f - timeLine.currentValue())); + } + if (clientRect.height() != fullRect.height() && clientRect.y() < area.y()) { + data.translate(0.0, - clientRect.height() * (1.0f - timeLine.currentValue())); + } + } + } + data.setRotationAngle(data.rotationAngle() * timeLine.currentValue()); + } + } + if (stop) { + if (w->isMinimized() && w != effects->activeWindow()) { + data.multiplyOpacity((1.0 - timeLine.currentValue())); + } else { + const QVector3D translation = data.translation() * (1.0 - timeLine.currentValue()); + 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.currentValue()); + } + if (clientRect.height() != fullRect.height() && clientRect.y() != fullRect.y()) { + data.translate(0.0, - clientRect.y() * timeLine.currentValue()); + } + } else { + if (clientRect.width() != fullRect.width() && clientRect.x() < rect.x()) { + data.translate(- clientRect.width() * timeLine.currentValue()); + } + if (clientRect.height() != fullRect.height() && clientRect.y() < area.y()) { + data.translate(0.0, - clientRect.height() * timeLine.currentValue()); + } + } + } + data.setRotationAngle(data.rotationAngle() * (1.0 - timeLine.currentValue())); + } + } + + 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.currentValue()); + } else if (stop) { + data.multiplyOpacity(1.0 - timeLine.currentValue()); + } + 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 == NULL) + 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.currentValue()); + data.setRotationAxis(Qt::YAxis); + data.setRotationAngle(-angle * timeLine.currentValue()); + 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.currentValue() * factor); + data.setRotationAxis(Qt::YAxis); + data.setRotationAngle(angle * timeLine.currentValue()); + } + } + if (specialHandlingForward) { + data.multiplyOpacity((1.0 - timeLine.currentValue() * 2.0)); + paintWindowCover(frontWindow, reflectedWindow, data); + } else + 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.currentValue() >= 0.5 && additionalWindow != NULL) { + 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.currentValue() - 0.5) * 2.0); + paintWindowCover(additionalWindow, reflectedWindows, data); + } + // normal behaviour + for (int i = 0; i < windows.count(); i++) { + window = windows.at(i); + if (window == NULL || 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.currentValue()); + data.setRotationAngle(angle - angle * timeLine.currentValue()); + } + // 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.currentValue()); + } + } 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.currentValue()); + data.setRotationAngle(angle - angle * timeLine.currentValue()); + } + // 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.currentValue()); + } + } + } + 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.currentValue() < 0.5) { + data.multiplyOpacity((1.0 - timeLine.currentValue() * 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::MidButton: + 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(0); + mActivated = false; + stop = false; + stopRequested = false; + effects->addRepaintFull(); + captionFrame->free(); +} + +void CoverSwitchEffect::slotWindowClosed(EffectWindow* c) +{ + if (c == selected_window) + selected_window = 0; + // 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..320c052 --- /dev/null +++ b/effects/coverswitch/coverswitch.h @@ -0,0 +1,167 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef KWIN_COVERSWITCH_H +#define KWIN_COVERSWITCH_H + +#include +#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(); + + virtual void reconfigure(ReconfigureFlags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual void postPaintScreen(); + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual void windowInputMouseEvent(QEvent* e); + virtual bool isActive() const; + + static bool supported(); + + // for properties + int configuredAnimationDuration() const { + return animationDuration; + } + 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 = NULL); + 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; + int animationDuration; + bool stopRequested; + bool startRequested; + QTimeLine 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..dbb0856 --- /dev/null +++ b/effects/coverswitch/coverswitch_config.cpp @@ -0,0 +1,67 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..40bf992 --- /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[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]=Cover Switch +Name[is]=Síðuskiptir +Name[it]=Scambiafinestre circolare +Name[ja]=カバースイッチ +Name[kk]=Cover Switch +Name[km]=ប្ដូរ​​គម្រប​ +Name[kn]=ಕವರ್ ಸ್ವಿಚ್ +Name[ko]=커버 전환기 +Name[lt]=VirÅ¡elių keitiklis +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[tg]=Циркуляция +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..7945de2 --- /dev/null +++ b/effects/coverswitch/coverswitch_config.h @@ -0,0 +1,54 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + +public Q_SLOTS: + virtual void save(); + +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..91be75f --- /dev/null +++ b/effects/cube/CMakeLists.txt @@ -0,0 +1,54 @@ +####################################### +# Effect + +# Data files +install( FILES + data/cubecap.png + DESTINATION ${DATA_INSTALL_DIR}/kwin ) + +####################################### +# Config + +# cube +set(kwin_cube_config_SRCS cube_config.cpp) +ki18n_wrap_ui(kwin_cube_config_SRCS cube_config.ui) +qt5_add_dbus_interface(kwin_cube_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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 + KF5::Service +) + +kcoreaddons_desktop_to_json(kwin_cube_config cube_config.desktop SERVICE_TYPES kcmodule.desktop) + +# cube slide +set(kwin_cubeslide_config_SRCS cubeslide_config.cpp) +ki18n_wrap_ui(kwin_cubeslide_config_SRCS cubeslide_config.ui) +qt5_add_dbus_interface(kwin_cubeslide_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +kconfig_add_kcfg_files(kwin_cubeslide_config_SRCS cubeslideconfig.kcfgc) + +add_library(kwin_cubeslide_config MODULE ${kwin_cubeslide_config_SRCS}) + +target_link_libraries(kwin_cubeslide_config + kwineffects + KF5::ConfigWidgets + KF5::I18n + KF5::Service +) + +kcoreaddons_desktop_to_json(kwin_cubeslide_config cubeslide_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_cube_config + kwin_cubeslide_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..64c622d --- /dev/null +++ b/effects/cube/cube.cpp @@ -0,0 +1,1740 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(NULL) + , reflection(true) + , desktopChangedWhileRotating(false) + , paintCaps(true) + , wallpaper(NULL) + , texturedCaps(true) + , capTexture(NULL) + , 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(0) + , sphereShader(0) + , zOrderingFactor(0.0f) + , mAddedHeightCoeff1(0.0f) + , mAddedHeightCoeff2(0.0f) + , m_cubeCapBuffer(NULL) + , 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 = NULL; + m_capShader = NULL; + } + m_textureMirrorMatrix.scale(1.0, -1.0, 1.0); + m_textureMirrorMatrix.translate(0.0, -1.0, 0.0); + connect(effects, SIGNAL(tabBoxAdded(int)), this, SLOT(slotTabBoxAdded(int))); + connect(effects, SIGNAL(tabBoxClosed()), this, SLOT(slotTabBoxClosed())); + connect(effects, SIGNAL(tabBoxUpdated()), this, SLOT(slotTabBoxUpdated())); + + 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 = NULL; + delete capTexture; + capTexture = NULL; + 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, SIGNAL(triggered(bool)), this, SLOT(toggleCube())); + connect(cylinderAction, SIGNAL(triggered(bool)), this, SLOT(toggleCylinder())); + connect(sphereAction, SIGNAL(triggered(bool)), this, SLOT(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 = NULL; + 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, QRegion region, 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() : NULL); +} + +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() : NULL); +} + +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() : NULL); +} + +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(0); + delete m_cubeCapBuffer; + m_cubeCapBuffer = NULL; + 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; + foreach (const QRect & paintRect, paint.rects()) { + 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(), NULL); + 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, SIGNAL(finished()), SLOT(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, SIGNAL(finished()), SLOT(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..8da87a6 --- /dev/null +++ b/effects/cube/cube.h @@ -0,0 +1,263 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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(); + virtual void reconfigure(ReconfigureFlags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual void postPaintScreen(); + virtual void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time); + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual bool borderActivated(ElectricBorder border); + virtual void grabbedKeyboardEvent(QKeyEvent* e); + virtual void windowInputMouseEvent(QEvent* e); + virtual bool isActive() const; + + int requestedEffectChainPosition() const override { + return 50; + } + + // proxy functions + virtual void* proxy(); + 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..422506d --- /dev/null +++ b/effects/cube/cube_config.cpp @@ -0,0 +1,121 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008 Martin Gräßlin + +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, see . +*********************************************************************/ +#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, SIGNAL(stateChanged(int)), this, SLOT(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..0be142b --- /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[be@latin]=Rabočy kub +Name[bg]=Кубичен работен плот +Name[bn]=ডেস্কটপ কিউব +Name[bs]=Kocka površi +Name[ca]=Cub d'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]=Desktop Cube +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[tg]=Мизи корӣ, монанди куб +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..378d7e1 --- /dev/null +++ b/effects/cube/cube_config.h @@ -0,0 +1,57 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + +public Q_SLOTS: + virtual void save(); + +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..b8223d0 --- /dev/null +++ b/effects/cube/cube_inside.h @@ -0,0 +1,40 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef KWIN_CUBE_INSIDE_H +#define KWIN_CUBE_INSIDE_H +#include + +namespace KWin +{ + +class CubeInsideEffect : public Effect +{ +public: + CubeInsideEffect() {} + virtual ~CubeInsideEffect() {} + + 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..b44918d --- /dev/null +++ b/effects/cube/cube_proxy.cpp @@ -0,0 +1,47 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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..87c9169 --- /dev/null +++ b/effects/cube/cube_proxy.h @@ -0,0 +1,45 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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/cubeslide.cpp b/effects/cube/cubeslide.cpp new file mode 100644 index 0000000..3d4db31 --- /dev/null +++ b/effects/cube/cubeslide.cpp @@ -0,0 +1,609 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#include "cubeslide.h" +// KConfigSkeleton +#include "cubeslideconfig.h" + +#include +#include + +#include + +#include + +namespace KWin +{ + +CubeSlideEffect::CubeSlideEffect() + : windowMoving(false) + , desktopChangedWhileMoving(false) + , progressRestriction(0.0f) +{ + initConfig(); + connect(effects, SIGNAL(desktopChanged(int,int)), this, SLOT(slotDesktopChanged(int,int))); + connect(effects, SIGNAL(windowStepUserMovedResized(KWin::EffectWindow*,QRect)), this, SLOT(slotWindowStepUserMovedResized(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowFinishUserMovedResized(KWin::EffectWindow*)), this, SLOT(slotWindowFinishUserMovedResized(KWin::EffectWindow*))); + 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.setCurveShape(QTimeLine::EaseInOutCurve); + timeLine.setDuration(rotationDuration); + dontSlidePanels = CubeSlideConfig::dontSlidePanels(); + dontSlideStickyWindows = CubeSlideConfig::dontSlideStickyWindows(); + usePagerLayout = CubeSlideConfig::usePagerLayout(); + useWindowMoving = CubeSlideConfig::useWindowMoving(); +} + +void CubeSlideEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (!slideRotations.empty()) { + data.mask |= PAINT_SCREEN_TRANSFORMED | Effect::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()); + if (dontSlidePanels) + panels.clear(); + stickyWindows.clear(); + } + effects->prePaintScreen(data, time); +} + +void CubeSlideEffect::paintScreen(int mask, QRegion region, ScreenPaintData& data) +{ + if (!slideRotations.empty()) { + glEnable(GL_CULL_FACE); + glCullFace(GL_FRONT); + paintSlideCube(mask, region, data); + glCullFace(GL_BACK); + paintSlideCube(mask, region, data); + glDisable(GL_CULL_FACE); + + if (dontSlidePanels) { + foreach (EffectWindow * w, panels) { + WindowPaintData wData(w); + effects->paintWindow(w, 0, infiniteRegion(), wData); + } + } + foreach (EffectWindow * w, stickyWindows) { + WindowPaintData wData(w); + effects->paintWindow(w, 0, infiniteRegion(), wData); + } + } 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 (!slideRotations.empty() && cube_painting) { + QRect rect = effects->clientArea(FullArea, effects->activeScreen(), painting_desktop); + if (dontSlidePanels && w->isDock()) { + w->setData(WindowForceBlurRole, QVariant(true)); + panels.insert(w); + } + if (!w->isManaged()) { + w->setData(WindowForceBlurRole, QVariant(true)); + stickyWindows.insert(w); + } else if (dontSlideStickyWindows && !w->isDock() && + !w->isDesktop() && w->isOnAllDesktops()) { + w->setData(WindowForceBlurRole, QVariant(true)); + stickyWindows.insert(w); + } + 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 (!slideRotations.empty() && cube_painting) { + if (dontSlidePanels && w->isDock()) + return; + if (stickyWindows.contains(w)) + return; + + // 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 (!slideRotations.empty()) { + 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.setCurveShape(QTimeLine::EaseOutCurve); + else + timeLine.setCurveShape(QTimeLine::LinearCurve); + if (slideRotations.empty()) { + foreach (EffectWindow * w, panels) + w->setData(WindowForceBlurRole, QVariant(false)); + foreach (EffectWindow * w, stickyWindows) + w->setData(WindowForceBlurRole, QVariant(false)); + stickyWindows.clear(); + panels.clear(); + effects->setActiveFullScreenEffect(0); + } + } + effects->addRepaintFull(); + } +} + +void CubeSlideEffect::slotDesktopChanged(int old, int current) +{ + 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) { + if (slideRotations.count() == 1) + timeLine.setCurveShape(QTimeLine::EaseInOutCurve); + else + timeLine.setCurveShape(QTimeLine::EaseInCurve); + effects->setActiveFullScreenEffect(this); + timeLine.setCurrentTime(0); + front_desktop = old; + effects->addRepaintFull(); + } +} + +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(0); + 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); + timeLine.setCurveShape(QTimeLine::EaseInOutCurve); + windowMoving = true; + effects->setActiveFullScreenEffect(this); + } + effects->addRepaintFull(); +} + +bool CubeSlideEffect::isActive() const +{ + return !slideRotations.isEmpty(); +} + +} // namespace diff --git a/effects/cube/cubeslide.h b/effects/cube/cubeslide.h new file mode 100644 index 0000000..92369f1 --- /dev/null +++ b/effects/cube/cubeslide.h @@ -0,0 +1,108 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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(); + virtual void reconfigure(ReconfigureFlags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual void postPaintScreen(); + virtual void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time); + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual bool isActive() const; + + 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 slotDesktopChanged(int old, int current); + void slotWindowStepUserMovedResized(KWin::EffectWindow *w); + void slotWindowFinishUserMovedResized(KWin::EffectWindow *w); + +private: + enum RotationDirection { + Left, + Right, + Upwards, + Downwards + }; + void paintSlideCube(int mask, QRegion region, ScreenPaintData& data); + void windowMovingChanged(float progress, RotationDirection direction); + bool cube_painting; + int front_desktop; + int painting_desktop; + int other_desktop; + bool firstDesktop; + QTimeLine timeLine; + QQueue slideRotations; + QSet panels; + QSet stickyWindows; + bool dontSlidePanels; + bool dontSlideStickyWindows; + bool usePagerLayout; + int rotationDuration; + bool useWindowMoving; + bool windowMoving; + bool desktopChangedWhileMoving; + double progressRestriction; +}; +} + +#endif diff --git a/effects/cube/cubeslide.kcfg b/effects/cube/cubeslide.kcfg new file mode 100644 index 0000000..3a6f2cb --- /dev/null +++ b/effects/cube/cubeslide.kcfg @@ -0,0 +1,25 @@ + + + + + + + 0 + + + true + + + false + + + true + + + false + + + diff --git a/effects/cube/cubeslide_config.cpp b/effects/cube/cubeslide_config.cpp new file mode 100644 index 0000000..3d78356 --- /dev/null +++ b/effects/cube/cubeslide_config.cpp @@ -0,0 +1,69 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ +#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/cube/cubeslide_config.desktop b/effects/cube/cubeslide_config.desktop new file mode 100644 index 0000000..ee73fee --- /dev/null +++ b/effects/cube/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[bg]=Кубична анимация върху работен плот +Name[bs]=Animacija kocke površi +Name[ca]=Animació de cub per a l'escriptori +Name[ca@valencia]=Animació de cub per a l'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]=Desktop Cube Animation +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[tg]=Мизи корӣ, монанди куб +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/cube/cubeslide_config.h b/effects/cube/cubeslide_config.h new file mode 100644 index 0000000..fa7f18d --- /dev/null +++ b/effects/cube/cubeslide_config.h @@ -0,0 +1,54 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + +public Q_SLOTS: + virtual void save(); + +private: + CubeSlideEffectConfigForm* m_ui; +}; + +} // namespace + +#endif diff --git a/effects/cube/cubeslide_config.ui b/effects/cube/cubeslide_config.ui new file mode 100644 index 0000000..44acfe9 --- /dev/null +++ b/effects/cube/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/cube/cubeslideconfig.kcfgc b/effects/cube/cubeslideconfig.kcfgc new file mode 100644 index 0000000..6059a7e --- /dev/null +++ b/effects/cube/cubeslideconfig.kcfgc @@ -0,0 +1,5 @@ +File=cubeslide.kcfg +ClassName=CubeSlideConfig +NameSpace=KWin +Singleton=true +Mutators=true 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..549108d --- /dev/null +++ b/effects/cube/data/1.10/cylinder.vert @@ -0,0 +1,46 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ +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..35f6549 --- /dev/null +++ b/effects/cube/data/1.10/sphere.vert @@ -0,0 +1,52 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ +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..a286126 --- /dev/null +++ b/effects/cube/data/1.40/cylinder.vert @@ -0,0 +1,47 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..28975ed --- /dev/null +++ b/effects/cube/data/1.40/sphere.vert @@ -0,0 +1,53 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..9904868688de535fd67131dc8393e0e5ac058460 GIT binary patch literal 305666 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

d(!gReU4dZO;{}4!+hK6Cy1gPGlvo$$Z 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 +Copyright (C) 2008 Lucas Murray +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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 + +namespace KWin +{ + +// WARNING, TODO: This effect relies on the desktop layout being EWMH-compliant. + +DesktopGridEffect::DesktopGridEffect() + : activated(false) + , timeline() + , keyboardGrab(false) + , wasWindowMove(false) + , wasDesktopMove(false) + , isValidMove(false) + , windowMove(NULL) + , windowMoveDiff() + , gridSize() + , orientation(Qt::Horizontal) + , activeCell(1, 1) + , scale() + , unscaledBorder() + , scaledSize() + , scaledOffset() + , m_proxy(0) + , 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, SIGNAL(triggered(bool)), this, SLOT(toggle())); + connect(KGlobalAccel::self(), &KGlobalAccel::globalShortcutChanged, this, &DesktopGridEffect::globalShortcutChanged); + connect(effects, SIGNAL(windowAdded(KWin::EffectWindow*)), this, SLOT(slotWindowAdded(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowClosed(KWin::EffectWindow*)), this, SLOT(slotWindowClosed(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowDeleted(KWin::EffectWindow*)), this, SLOT(slotWindowDeleted(KWin::EffectWindow*))); + connect(effects, SIGNAL(numberDesktopsChanged(uint)), this, SLOT(slotNumberDesktopsChanged(uint))); + connect(effects, SIGNAL(windowGeometryShapeChanged(KWin::EffectWindow*,QRect)), this, SLOT(slotWindowGeometryShapeChanged(KWin::EffectWindow*,QRect))); + connect(effects, &EffectsHandler::numberScreensChanged, this, &DesktopGridEffect::setup); + + // Load all other configuration details + reconfigure(ReconfigureAll); +} + +DesktopGridEffect::~DesktopGridEffect() +{ + foreach (DesktopButtonsView *view, m_desktopButtonsViews) + view->deleteLater(); + m_desktopButtonsViews.clear(); +} + +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.setCurveShape(QTimeLine::EaseInOutCurve); + 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, QRegion region, 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 + foreach (DesktopButtonsView *view, m_desktopButtonsViews) { + if (!view->effectWindow) { + EffectWindow *viewWindow = effects->findWindow(view->winId()); + if (viewWindow) { + viewWindow->setData(WindowForceBlurRole, QVariant(true)); + view->effectWindow = viewWindow; + } + } + if (view->effectWindow) { + WindowPaintData d(view->effectWindow); + d.multiplyOpacity(timeline.currentValue()); + effects->drawWindow(view->effectWindow, PAINT_WINDOW_TRANSLUCENT, infiniteRegion(), d); + } + } + + 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) { + return; // will be painted on top of all other windows + } + foreach (DesktopButtonsView *view, m_desktopButtonsViews) { + if (view->effectWindow == w) { + if (!activated && timeline.currentValue() < 0.05) { + view->hide(); + } + 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 = NULL; + } + 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 = 0; + foreach (DesktopButtonsView *view, m_desktopButtonsViews) { + if (view->effectWindow && view->effectWindow == w) { + view->effectWindow = nullptr; + break; + } + } + if (isUsingPresentWindows()) { + for (QList::iterator it = m_managers.begin(), + end = m_managers.end(); it != end; ++it) { + it->unmanage(w); + } + } +} + +void DesktopGridEffect::slotWindowGeometryShapeChanged(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)) { + foreach (DesktopButtonsView *view, m_desktopButtonsViews) { + if (view->geometry().contains(me->pos())) { + const QPoint widgetPos = view->mapFromGlobal(me->pos()); + QMouseEvent event(me->type(), widgetPos, me->pos(), me->button(), me->buttons(), me->modifiers()); + view->windowInputMouseEvent(&event); + return; + } + } + } + + if (e->type() == QEvent::MouseMove) { + int d = posToDesktop(me->pos()); + if (windowMove != NULL && + (me->pos() - dragStartPos).manhattanLength() > QApplication::startDragDistance()) { + // Handle window moving + if (!wasWindowMove) { // Activate on move + if (isUsingPresentWindows()) { + foreach (const int i, desktopList(windowMove)) { + const int sourceDesktop = windowMove->isOnAllDesktops() ? d : windowMove->desktop(); + 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(), NULL) + windowMoveDiff, true, 1.0 / scale[screen]); + } + if (wasWindowMove) { + effects->defineCursor(Qt::ClosedHandCursor); + if (d != highlightedDesktop) { + effects->windowToDesktop(windowMove, d); // Not true all desktop move + 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]) { + effects->windowToDesktop(w, desks[i+1]); + 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(); + bool isDesktop = (me->modifiers() & Qt::ControlModifier); + EffectWindow* w = isDesktop ? NULL : windowAt(me->pos()); + if (w != NULL) + isDesktop = w->isDesktop(); + if (isDesktop) + m_originalMovingDesktop = posToDesktop(me->pos()); + if (w != NULL && !w->isDesktop() && (w->isMovable() || w->isMovableAcrossScreens() || isUsingPresentWindows())) { + // Prepare it for moving + windowMoveDiff = w->pos() - unscalePos(me->pos(), NULL); + windowMove = w; + effects->setElevatedWindow(windowMove, true); + } + } else if ((me->buttons() == Qt::MidButton || me->buttons() == Qt::RightButton) && windowMove == NULL) { + EffectWindow* w = windowAt(me->pos()); + if (w && w->isDesktop()) { + w = nullptr; + } + if (w != NULL) { + int desktop = 0; + if (w->isOnAllDesktops()) { + desktop = posToDesktop(me->pos()); + effects->windowToDesktop(w, desktop); + } else { + desktop = w->desktop(); + 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 = windowMove->isOnAllDesktops() ? posToDesktop(cursorPos()) : windowMove->desktop(); + 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 = NULL; + } + wasWindowMove = false; + wasDesktopMove = false; + } +} + +void DesktopGridEffect::grabbedKeyboardEvent(QKeyEvent* e) +{ + if (timeline.currentValue() != 1) // Block user input during animations + return; + if (windowMove != NULL) + 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 != NULL) { + 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 NULL; + 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 NULL; +} + +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; + foreach (DesktopButtonsView *view, m_desktopButtonsViews) { + 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->setCurveShape(QTimeLine::EaseInOutCurve); + 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); + } + } + } + bool enableAdd = effects->numberOfDesktops() < 20; + bool enableRemove = effects->numberOfDesktops() > 1; + + QVector::iterator it = m_desktopButtonsViews.begin(); + const int n = DesktopGridConfig::showAddRemove() ? effects->numScreens() : 0; + for (int i = 0; i < n; ++i) { + DesktopButtonsView *view; + if (it == m_desktopButtonsViews.end()) { + view = new DesktopButtonsView(); + m_desktopButtonsViews.append(view); + it = m_desktopButtonsViews.end(); // changed through insert! + connect(view, SIGNAL(addDesktop()), SLOT(slotAddDesktop())); + connect(view, SIGNAL(removeDesktop()), SLOT(slotRemoveDesktop())); + } else { + view = *it; + ++it; + } + view->setAddDesktopEnabled(enableAdd); + view->setRemoveDesktopEnabled(enableRemove); + const QRect screenRect = effects->clientArea(FullScreenArea, i, 1); + view->show(); // pseudo show must happen before geometry changes + view->setPosition(screenRect.right() - border/3 - view->width(), + screenRect.bottom() - border/3 - view->height()); + } + while (it != m_desktopButtonsViews.end()) { + (*it)->deleteLater(); + it = m_desktopButtonsViews.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(0); + if (isUsingPresentWindows()) { + while (!m_managers.isEmpty()) { + m_managers.first().unmanageAll(); + m_managers.removeFirst(); + } + m_proxy = 0; + } +} + +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 != NULL); +} + +// 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(); + bool enableAdd = desktop < 20; + bool enableRemove = desktop > 1; + foreach (DesktopButtonsView *view, m_desktopButtonsViews) { + view->setAddDesktopEnabled(enableAdd); + view->setRemoveDesktopEnabled(enableRemove); + } + 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->setCurveShape(QTimeLine::EaseInOutCurve); + 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(); +} + +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; + } + + if (w->desktop() > effects->numberOfDesktops() || w->desktop() < 1) { // sic! desktops are [1,n] + static QVector emptyVector; + emptyVector.resize(0); + return emptyVector; + } + + static QVector singleDesktop; + singleDesktop.resize(1); + singleDesktop[0] = w->desktop() - 1; + return singleDesktop; +} + +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->isCurrentTab()) { + return false; + } + + if (!w->isOnCurrentActivity()) { + return false; + } + + return true; +} + +/************************************************ +* DesktopButtonView +************************************************/ +DesktopButtonsView::DesktopButtonsView(QWindow *parent) + : QQuickView(parent) + , effectWindow(nullptr) + , m_visible(false) + , m_posIsValid(false) +{ + setFlags(Qt::X11BypassWindowManagerHint | Qt::FramelessWindowHint); + setColor(Qt::transparent); + + rootContext()->setContextProperty(QStringLiteral("add"), QVariant(true)); + rootContext()->setContextProperty(QStringLiteral("remove"), QVariant(true)); + setSource(QUrl(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kwin/effects/desktopgrid/main.qml")))); + if (QObject *item = rootObject()->findChild(QStringLiteral("addButton"))) { + connect(item, SIGNAL(clicked()), SIGNAL(addDesktop())); + } + if (QObject *item = rootObject()->findChild(QStringLiteral("removeButton"))) { + connect(item, SIGNAL(clicked()), SIGNAL(removeDesktop())); + } +} + +void DesktopButtonsView::windowInputMouseEvent(QMouseEvent *e) +{ + if (e->type() == QEvent::MouseMove) { + mouseMoveEvent(e); + } else if (e->type() == QEvent::MouseButtonPress) { + mousePressEvent(e); + } else if (e->type() == QEvent::MouseButtonDblClick) { + mouseDoubleClickEvent(e); + } else if (e->type() == QEvent::MouseButtonRelease) { + mouseReleaseEvent(e); + } +} + +void DesktopButtonsView::setAddDesktopEnabled(bool enable) +{ + rootContext()->setContextProperty(QStringLiteral("add"), QVariant(enable)); +} + +void DesktopButtonsView::setRemoveDesktopEnabled(bool enable) +{ + rootContext()->setContextProperty(QStringLiteral("remove"), QVariant(enable)); +} + +bool DesktopButtonsView::isVisible() const +{ + return m_visible; +} + +void DesktopButtonsView::show() +{ + if (!m_visible && m_posIsValid) { + setPosition(m_pos); + m_posIsValid = false; + } + m_visible = true; + QQuickView::show(); +} + +void DesktopButtonsView::hide() +{ + if (!m_posIsValid) { + m_pos = position(); + m_posIsValid = true; + setPosition(-width(), -height()); + } + m_visible = false; +} + +} // namespace + diff --git a/effects/desktopgrid/desktopgrid.h b/effects/desktopgrid/desktopgrid.h new file mode 100644 index 0000000..3c95eee --- /dev/null +++ b/effects/desktopgrid/desktopgrid.h @@ -0,0 +1,192 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak +Copyright (C) 2008 Lucas Murray + +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, see . +*********************************************************************/ + +#ifndef KWIN_DESKTOPGRID_H +#define KWIN_DESKTOPGRID_H + +#include +#include +#include +#include + +namespace KWin +{ + +class PresentWindowsEffectProxy; + +class DesktopButtonsView : public QQuickView +{ + Q_OBJECT +public: + explicit DesktopButtonsView(QWindow *parent = 0); + void windowInputMouseEvent(QMouseEvent* e); + void setAddDesktopEnabled(bool enable); + void setRemoveDesktopEnabled(bool enable); + bool isVisible() const; + void show(); + void hide(); +public: + EffectWindow *effectWindow; +Q_SIGNALS: + void addDesktop(); + void removeDesktop(); +private: + bool m_visible; + QPoint m_pos; + bool m_posIsValid; +}; + +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(); + virtual void reconfigure(ReconfigureFlags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual void postPaintScreen(); + virtual void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time); + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual void windowInputMouseEvent(QEvent* e); + virtual void grabbedKeyboardEvent(QKeyEvent* e); + virtual bool borderActivated(ElectricBorder border); + virtual bool isActive() const; + + 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 slotWindowGeometryShapeChanged(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 = NULL) 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 m_originalMovingDesktop; + bool keyboardGrab; + bool wasWindowMove, 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_desktopButtonsViews; + + 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..3b17535 --- /dev/null +++ b/effects/desktopgrid/desktopgrid_config.cpp @@ -0,0 +1,140 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2008 Lucas Murray + +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, see . +*********************************************************************/ + +#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(0))); + 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, SIGNAL(currentIndexChanged(int)), this, SLOT(layoutSelectionChanged())); + connect(m_ui->desktopNameAlignmentCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(changed())); + connect(m_ui->shortcutEditor, SIGNAL(keyChange()), this, SLOT(changed())); + + 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..de50010 --- /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[be]=Сетка на працоўны стол +Name[be@latin]=Rabočaja sietka +Name[bg]=Мрежест работен плот +Name[bn]=ডেস্কটপ গ্রিড +Name[bs]=Mreža površi +Name[ca]=Quadrícula d'escriptori +Name[ca@valencia]=Quadrícula 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]=Desktop Grid +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 da área de trabalho +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[tg]=Шабакаи мизи корӣ +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..8bd8727 --- /dev/null +++ b/effects/desktopgrid/desktopgrid_config.h @@ -0,0 +1,62 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2008 Lucas Murray + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + ~DesktopGridEffectConfig(); + +public Q_SLOTS: + virtual void save(); + virtual void load(); + virtual void defaults(); + +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..568a296 --- /dev/null +++ b/effects/desktopgrid/main.qml @@ -0,0 +1,49 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +import QtQuick 2.0 +import org.kde.plasma.components 2.0 as Plasma + +Item { + width: childrenRect.width + height: childrenRect.height + Plasma.ButtonRow { + exclusive: false + width: childrenRect.width + height: childrenRect.height + Plasma.Button { + id: removeButton + objectName: "removeButton" + enabled: remove + width: height + font.bold: true + font.pointSize: 20 + text: "-" + } + Plasma.Button { + id: addButton + objectName: "addButton" + enabled: add + font.bold: true + font.pointSize: 20 + width: height + text: "+" + } + } +} diff --git a/effects/dialogparent/CMakeLists.txt b/effects/dialogparent/CMakeLists.txt new file mode 100644 index 0000000..1242620 --- /dev/null +++ b/effects/dialogparent/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(package) diff --git a/effects/dialogparent/package/CMakeLists.txt b/effects/dialogparent/package/CMakeLists.txt new file mode 100644 index 0000000..44c284a --- /dev/null +++ b/effects/dialogparent/package/CMakeLists.txt @@ -0,0 +1,6 @@ +install(DIRECTORY contents DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_dialogparent) +install(FILES metadata.desktop DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_dialogparent) + +install(FILES metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_effect_dialogparent.desktop) diff --git a/effects/dialogparent/package/contents/code/main.js b/effects/dialogparent/package/contents/code/main.js new file mode 100644 index 0000000..2ca3c0e --- /dev/null +++ b/effects/dialogparent/package/contents/code/main.js @@ -0,0 +1,142 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +/*global effect, effects, animate, cancel, set, animationTime, Effect, QEasingCurve */ +/*jslint continue: true */ +var dialogParentEffect = { + duration: animationTime(300), + windowAdded: function (window) { + "use strict"; + if (window === null || window.modal === false) { + return; + } + dialogParentEffect.dialogGotModality(window) + }, + dialogGotModality: function (window) { + "use strict"; + var mainWindows = window.mainWindows(); + for (var i = 0; i < mainWindows.length; i += 1) { + var w = mainWindows[i]; + if (w.dialogParentAnimation !== undefined) { + continue; + } + dialogParentEffect.startAnimation(w, dialogParentEffect.duration); + } + }, + startAnimation: function (window, duration) { + "use strict"; + if (window.visible === false) { + return; + } + window.dialogParentAnimation = set({ + window: window, + duration: duration, + animations: [{ + type: Effect.Saturation, + to: 0.4 + }, { + type: Effect.Brightness, + to: 0.6 + }] + }); + }, + windowClosed: function (window) { + "use strict"; + dialogParentEffect.cancelAnimation(window); + if (window.modal === false) { + return; + } + dialogParentEffect.dialogLostModality(window); + }, + dialogLostModality: function (window) { + "use strict"; + var mainWindows = window.mainWindows(); + for (var i = 0; i < mainWindows.length; i += 1) { + var w = mainWindows[i]; + if (w.dialogParentAnimation === undefined) { + continue; + } + cancel(w.dialogParentAnimation); + w.dialogParentAnimation = undefined; + animate({ + window: w, + duration: dialogParentEffect.duration, + animations: [{ + type: Effect.Saturation, + from: 0.4, + to: 1.0 + }, { + type: Effect.Brightness, + from: 0.6, + to: 1.0 + }] + }); + } + }, + cancelAnimation: function (window) { + "use strict"; + if (window.dialogParentAnimation !== undefined) { + cancel(window.dialogParentAnimation); + window.dialogParentAnimation = undefined; + } + }, + desktopChanged: function () { + "use strict"; + var i, windows, window; + windows = effects.stackingOrder; + for (i = 0; i < windows.length; i += 1) { + window = windows[i]; + dialogParentEffect.cancelAnimation(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, 1); + }, + init: function () { + "use strict"; + var i, windows; + effects.windowAdded.connect(dialogParentEffect.windowAdded); + effects.windowClosed.connect(dialogParentEffect.windowClosed); + effects.windowMinimized.connect(dialogParentEffect.cancelAnimation); + effects.windowUnminimized.connect(dialogParentEffect.restartAnimation); + effects.windowModalityChanged.connect(dialogParentEffect.modalDialogChanged) + effects['desktopChanged(int,int)'].connect(dialogParentEffect.desktopChanged); + effects.desktopPresenceChanged.connect(dialogParentEffect.cancelAnimation); + effects.desktopPresenceChanged.connect(dialogParentEffect.restartAnimation); + + // 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..7da7cee --- /dev/null +++ b/effects/dialogparent/package/metadata.desktop @@ -0,0 +1,159 @@ +[Desktop Entry] +Name=Dialog Parent +Name[af]=Voorafgaande dialoog +Name[ar]=مولدة الحوار +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]=Dialog Parent +Name[is]=Dekking undir valglugga +Name[it]=Finestra madre +Name[ja]=ダイアログの親 +Name[kk]=Диалогтың аталығы +Name[km]=ប្រអប់​មេ​ +Name[kn]=ಸಂವಾದ ಪೂರ್ವಜ (ಪೇರೆಂಟ್) +Name[ko]=대화 상자 부모 +Name[lt]=Dialogo savininkas +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[tg]=Соҳиби диалог +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[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 jendela 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 dialogo savininką kai aktyvuojamas dialogo langas +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-Depends= +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..f19f36a --- /dev/null +++ b/effects/diminactive/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_diminactive_config_SRCS diminactive_config.cpp) +ki18n_wrap_ui(kwin_diminactive_config_SRCS diminactive_config.ui) +qt5_add_dbus_interface(kwin_diminactive_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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 + Qt5::DBus + KF5::ConfigWidgets + KF5::I18n + KF5::Service +) + +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..69ba653 --- /dev/null +++ b/effects/diminactive/diminactive.cpp @@ -0,0 +1,398 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak +Copyright (C) 2007 Christian Nitschkowski +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +// 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); +} + +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(); + + // Need to reset m_activeWindow becase canDimWindow returns false + // if m_activeWindow is equal to effects->activeWindow(). + m_activeWindow = nullptr; + + EffectWindow *activeWindow = effects->activeWindow(); + m_activeWindow = (activeWindow && canDimWindow(activeWindow)) + ? activeWindow + : nullptr; + + 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->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(); +} + +} // namespace KWin diff --git a/effects/diminactive/diminactive.h b/effects/diminactive/diminactive.h new file mode 100644 index 0000000..1af62f7 --- /dev/null +++ b/effects/diminactive/diminactive.h @@ -0,0 +1,130 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak +Copyright (C) 2007 Christian Nitschkowski +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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) + +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; + +private Q_SLOTS: + void windowActivated(EffectWindow *w); + void windowClosed(EffectWindow *w); + void windowDeleted(EffectWindow *w); + void activeFullScreenEffectChanged(); + +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; + + EffectWindow *m_activeWindow; + 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; +} + +} // namespace KWin + +#endif diff --git a/effects/diminactive/diminactive.kcfg b/effects/diminactive/diminactive.kcfg new file mode 100644 index 0000000..f1f32fe --- /dev/null +++ b/effects/diminactive/diminactive.kcfg @@ -0,0 +1,24 @@ + + + + + + 25 + + + false + + + false + + + false + + + true + + + diff --git a/effects/diminactive/diminactive_config.cpp b/effects/diminactive/diminactive_config.cpp new file mode 100644 index 0000000..03f2cd5 --- /dev/null +++ b/effects/diminactive/diminactive_config.cpp @@ -0,0 +1,65 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Christian Nitschkowski +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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..dbaf575 --- /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[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]=Dim Inactive +Name[is]=Dimma óvirka +Name[it]=Scurisci le inattive +Name[ja]=非アクティブを暗く +Name[kk]=Белсенді еместі күңгірттеу +Name[km]=បន្ថយ​ពន្លឺ​នៅពេល​អសកម្ម +Name[kn]=ನಿಷ್ಕ್ರಿಯವಾದದ್ದನ್ನು ಮಂಕಾಗಿಸು +Name[ko]=비활성 창 어둡게 +Name[lt]=Užtemdomi neaktyvūs langai +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[tg]=Затемнение отключено +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..3a2fad6 --- /dev/null +++ b/effects/diminactive/diminactive_config.h @@ -0,0 +1,48 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Christian Nitschkowski +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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..19f7ff1 --- /dev/null +++ b/effects/diminactive/diminactive_config.ui @@ -0,0 +1,76 @@ + + + DimInactiveEffectConfig + + + + 0 + 0 + 400 + 160 + + + + + + + Strength: + + + + + + + + 0 + 0 + + + + 100 + + + 5 + + + + + + + Dim: + + + + + + + Docks and panels + + + + + + + Desktop + + + + + + + Keep above windows + + + + + + + By window group + + + + + + + + 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/CMakeLists.txt b/effects/dimscreen/CMakeLists.txt new file mode 100644 index 0000000..1f7b191 --- /dev/null +++ b/effects/dimscreen/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# Effect + +# Source files +set( kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + dimscreen/dimscreen.cpp + ) diff --git a/effects/dimscreen/dimscreen.cpp b/effects/dimscreen/dimscreen.cpp new file mode 100644 index 0000000..a7f4f7e --- /dev/null +++ b/effects/dimscreen/dimscreen.cpp @@ -0,0 +1,119 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008, 2009 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "dimscreen.h" + +#include + +#include + +namespace KWin +{ + +static const QSet s_authWindows { + QStringLiteral("kdesu kdesu"), + QStringLiteral("kdesudo kdesudo"), + QStringLiteral("pinentry pinentry"), + QStringLiteral("polkit-kde-authentication-agent-1 polkit-kde-authentication-agent-1"), + QStringLiteral("polkit-kde-manager polkit-kde-manager"), +}; + +DimScreenEffect::DimScreenEffect() + : mActivated(false) + , activateAnimation(false) + , deactivateAnimation(false) +{ + reconfigure(ReconfigureAll); + connect(effects, SIGNAL(windowActivated(KWin::EffectWindow*)), this, SLOT(slotWindowActivated(KWin::EffectWindow*))); +} + +DimScreenEffect::~DimScreenEffect() +{ +} + +void DimScreenEffect::reconfigure(ReconfigureFlags) +{ + timeline.setDuration(animationTime(250)); +} + +void DimScreenEffect::prePaintScreen(ScreenPrePaintData& data, int time) +{ + if (mActivated && activateAnimation && !effects->activeFullScreenEffect()) + timeline.setCurrentTime(timeline.currentTime() + time); + if (mActivated && deactivateAnimation) + timeline.setCurrentTime(timeline.currentTime() - time); + if (mActivated && effects->activeFullScreenEffect()) + timeline.setCurrentTime(timeline.currentTime() - time); + if (mActivated && !activateAnimation && !deactivateAnimation && !effects->activeFullScreenEffect() && timeline.currentValue() != 1.0) + timeline.setCurrentTime(timeline.currentTime() + time); + effects->prePaintScreen(data, time); +} + +void DimScreenEffect::postPaintScreen() +{ + if (mActivated) { + if (activateAnimation && timeline.currentValue() == 1.0) { + activateAnimation = false; + effects->addRepaintFull(); + } + if (deactivateAnimation && timeline.currentValue() == 0.0) { + deactivateAnimation = false; + mActivated = false; + effects->addRepaintFull(); + } + // still animating + if (timeline.currentValue() > 0.0 && timeline.currentValue() < 1.0) + effects->addRepaintFull(); + } + effects->postPaintScreen(); +} + +void DimScreenEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) +{ + if (mActivated && (w != window) && w->isManaged()) { + data.multiplyBrightness((1.0 - 0.33 * timeline.currentValue())); + data.multiplySaturation((1.0 - 0.33 * timeline.currentValue())); + } + effects->paintWindow(w, mask, region, data); +} + +void DimScreenEffect::slotWindowActivated(EffectWindow *w) +{ + if (!w) return; + if (s_authWindows.contains(w->windowClass())) { + mActivated = true; + activateAnimation = true; + deactivateAnimation = false; + window = w; + effects->addRepaintFull(); + } else { + if (mActivated) { + activateAnimation = false; + deactivateAnimation = true; + effects->addRepaintFull(); + } + } +} + +bool DimScreenEffect::isActive() const +{ + return mActivated; +} + +} // namespace diff --git a/effects/dimscreen/dimscreen.h b/effects/dimscreen/dimscreen.h new file mode 100644 index 0000000..556aafc --- /dev/null +++ b/effects/dimscreen/dimscreen.h @@ -0,0 +1,57 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008, 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef KWIN_DIMSCREEN_H +#define KWIN_DIMSCREEN_H + +#include +#include + +namespace KWin +{ + +class DimScreenEffect + : public Effect +{ + Q_OBJECT +public: + DimScreenEffect(); + ~DimScreenEffect(); + + virtual void reconfigure(ReconfigureFlags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void postPaintScreen(); + virtual void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data); + virtual bool isActive() const; + +public Q_SLOTS: + void slotWindowActivated(KWin::EffectWindow *w); + +private: + bool mActivated; + bool activateAnimation; + bool deactivateAnimation; + QTimeLine timeline; + EffectWindow* window; +}; + +} // namespace + +#endif diff --git a/effects/effect_builtins.cpp b/effects/effect_builtins.cpp new file mode 100644 index 0000000..7b4b7f7 --- /dev/null +++ b/effects/effect_builtins.cpp @@ -0,0 +1,807 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 "dimscreen/dimscreen.h" +#include "fallapart/fallapart.h" +#include "highlightwindow/highlightwindow.h" +#include "magiclamp/magiclamp.h" +#include "minimizeanimation/minimizeanimation.h" +#include "resize/resize.h" +#include "scale/scale.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 "cube/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("http://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("http://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("http://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("http://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("http://files.kde.org/plasma/kwin/effect-videos/dim_inactive.mp4")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + nullptr, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("dimscreen"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Dim Screen for Administrator Mode"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Darkens the entire screen when requesting root privileges"), + QStringLiteral("Focus"), + QString(), + QUrl(QStringLiteral("http://files.kde.org/plasma/kwin/effect-videos/dim_administration.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("Candy"), + 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("http://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("Appearance"), + QString(), + 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("http://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("http://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("http://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("http://files.kde.org/plasma/kwin/effect-videos/magnifier.ogv")), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &MagnifierEffect::supported, + nullptr +#endif +EFFECT_FALLBACK + }, { + QStringLiteral("minimizeanimation"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Minimize Animation"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Animate the minimizing of windows"), + QStringLiteral("Appearance"), + QStringLiteral("minimize"), + QUrl(QStringLiteral("http://files.kde.org/plasma/kwin/effect-videos/minimize.ogv")), + true, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &MinimizeAnimationEffect::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("http://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("http://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("scale"), + i18ndc("kwin_effects", "Name of a KWin Effect", "Scale"), + i18ndc("kwin_effects", "Comment describing the KWin Effect", "Make windows smoothly scale in and out when they are shown or hidden"), + QStringLiteral("Appearance"), + QString(), + QUrl(), + false, + false, +#ifdef EFFECT_BUILTINS + &createHelper, + &ScaleEffect::supported, + 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 KSnapshot"), + 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("Candy"), + 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("http://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("http://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("http://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("Candy"), + 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("http://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("Candy"), + QString(), + QUrl(QStringLiteral("http://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("http://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..7abf4ac --- /dev/null +++ b/effects/effect_builtins.h @@ -0,0 +1,110 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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, + DimScreen, + FallApart, + FlipSwitch, + Glide, + HighlightWindow, + Invert, + Kscreen, + LookingGlass, + MagicLamp, + Magnifier, + MinimizeAnimation, + MouseClick, + MouseMark, + PresentWindows, + Resize, + Scale, + 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/CMakeLists.txt b/effects/eyeonscreen/CMakeLists.txt new file mode 100644 index 0000000..28f40b3 --- /dev/null +++ b/effects/eyeonscreen/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory( package ) diff --git a/effects/eyeonscreen/package/CMakeLists.txt b/effects/eyeonscreen/package/CMakeLists.txt new file mode 100644 index 0000000..37dc0c7 --- /dev/null +++ b/effects/eyeonscreen/package/CMakeLists.txt @@ -0,0 +1,6 @@ +install(DIRECTORY contents DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_eyeonscreen) +install(FILES metadata.desktop DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_eyeonscreen) + +install(FILES metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_effect_eyeonscreen.desktop) diff --git a/effects/eyeonscreen/package/contents/code/main.js b/effects/eyeonscreen/package/contents/code/main.js new file mode 100644 index 0000000..f6c51b8 --- /dev/null +++ b/effects/eyeonscreen/package/contents/code/main.js @@ -0,0 +1,165 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2015 Thomas Lübking + +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, see . +*********************************************************************/ +/*global effect, effects, animate, animationTime, Effect, QEasingCurve */ + +var eyeOnScreenEffect = { + duration: animationTime(250), + loadConfig: function () { + "use strict"; + eyeOnScreenEffect.duration = animationTime(250); + }, + delevateWindow: function(window) { + "use strict"; + 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) { + "use strict"; + 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 () { + "use strict"; + eyeOnScreenEffect.loadConfig(); + effects.showingDesktopChanged.connect(eyeOnScreenEffect.slurp); + effect.animationEnded.connect(eyeOnScreenEffect.delevateWindow); + } +}; + +eyeOnScreenEffect.init(); \ No newline at end of file diff --git a/effects/eyeonscreen/package/metadata.desktop b/effects/eyeonscreen/package/metadata.desktop new file mode 100644 index 0000000..99164c9 --- /dev/null +++ b/effects/eyeonscreen/package/metadata.desktop @@ -0,0 +1,95 @@ +[Desktop Entry] +Name=eye On Screen +Name[ca]=Ull a la pantalla +Name[ca@valencia]=Ull a la pantalla +Name[cs]=oko na obrazovce +Name[da]=øjet på skærmen +Name[de]=Ansicht der Arbeitsfläche +Name[el]=μάτι Στην Οθόνη +Name[en_GB]=eye On Screen +Name[es]=Ojo a la pantalla +Name[et]=eye On Screen +Name[eu]=begia pantailan +Name[fi]=eye On Screen +Name[fr]=Jeter un oeil sur le bureau +Name[gl]=Ollo na pantalla +Name[he]=עין על המסך +Name[hu]=Ide figyelj! +Name[id]=eye On Screen +Name[it]=eye On Screen +Name[ja]=eye On Screen +Name[ko]=화면 위의 눈 +Name[nb]=øye på Skjerm +Name[nl]=eye On Screen +Name[nn]=Auge på skjerm +Name[pa]=ਸਕਰੀਨ 'ਤੇ ਅੱਖ +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[sr]=Око на екрану +Name[sr@ijekavian]=Око на екрану +Name[sr@ijekavianlatin]=Oko na ekranu +Name[sr@latin]=Oko na ekranu +Name[sv]=Ögat på skärmen +Name[tr]=Ekrandaki göz +Name[uk]=Око на екрані +Name[x-test]=xxeye On Screenxx +Name[zh_CN]=注视屏幕 +Name[zh_TW]=螢幕之眼 +Icon=preferences-system-windows-effect-eyeonscreen +Comment=Suck windows into desktop to show the latter. This might remind you of something. +Comment[ca]=Enganxa les finestres a l'escriptori per a mostrar la darrera. Això podria recordar-vos quelcom. +Comment[ca@valencia]=Enganxa les finestres a l'escriptori per a mostrar la darrera. Això podria recordar-vos alguna cosa. +Comment[da]=Sug vinduer ind i skrivebordet for at vise dette. Måske minder det dig om noget. +Comment[de]=Saugt Fenster in die Arbeitsfläche, um sie anzuzeigen. +Comment[el]=Απορρόφησε το παράθυρο στην επιφάνεια εργασίας για να δείξεις το τελευταίο. Αυτό ίσως σου υπενθυμίσει κάτι. +Comment[en_GB]=Suck windows into desktop to show the latter. This might remind you of something. +Comment[es]=Succionar las ventanas en el escritorio para mostrarlo. Esto debe recordarle algo. +Comment[et]=Imeb aknad töölauda ja näitab viimast. See võib midagi meenutada ... +Comment[eu]=Xurgatu mahaigaineko leihoak, berau erakusteko. Honek zer edo zer gogoraraziko diezazuke. +Comment[fi]=Imee ikkunat työpöytään näyttääkseen sen. Tämä saattaa muistuttaa sinua jostain. +Comment[fr]=Aspire toutes les fenêtres pour afficher le bureau. +Comment[gl]=Que o escritorio succione as xanelas para mostrarse. Isto pode que lle recorde a algo. +Comment[he]=שואב חלונות לשולחן עבודה כדי להראות את האחרון, זה אולי יזכיר לך משהו +Comment[hu]=Az asztalba szippantja az ablakokat az asztal megjelenítéséhez, hogy ezzel valami fontosra emlékeztesse. +Comment[id]=Cucutkan jendela ke desktop untuk tampil kelak. Ini mungkin mengingatkanmu tentang sesuatu. +Comment[it]=Risucchia le finestre nel desktop per mostrare l'ultima. Potrebbe ricordarti qualcosa. +Comment[ko]=창을 데스크톱으로 흡수시켜 데스크톱을 표시합니다. +Comment[nb]=Sug vinduer inn i skrivebordet for å vise det. Dette minner deg kanskje om noe. +Comment[nl]=Laat vensters wegzinken in het bureaublad om deze te tonen. Dit kan u aan iets herinneren. +Comment[nn]=Sug vindauge inn i skrivebordet for å visa skrivebordet. Dette minner deg kanskje om noko. +Comment[pl]=Zasysa okna, aby pokazać pulpit. Skojarzenia powinny nasuwać się same. +Comment[pt]=Suga as janelas para o ecrã mostrar a última. Isto podê-lo-á recordar de algo. +Comment[pt_BR]=Suga as janelas na área de trabalho para mostrar a última. Isso pode lembrá-lo de algo. +Comment[ru]=Для просмотра рабочего стола окна временно втягиваются в центр экрана +Comment[sk]=Prisaje okná na plochu na zobrazenie druhého. +Comment[sl]=Posrka okna v namizje, da je to prikazano. To vas bo morda na nekaj spomnilo. +Comment[sr]=Усисава прозоре у површ да би је приказао. Можда вас подсети на нешто. +Comment[sr@ijekavian]=Усисава прозоре у површ да би је приказао. Можда вас подсети на нешто. +Comment[sr@ijekavianlatin]=Usisava prozore u površ da bi je prikazao. Možda vas podseti na nešto. +Comment[sr@latin]=Usisava prozore u površ da bi je prikazao. Možda vas podseti na nešto. +Comment[sv]=Sug in fönster i skrivbordet för att visa det senare. Det kanske påminner dig om någonting. +Comment[tr]=İlerisini göstermek için pencereleri masaüstüne topla. Bu size bir şey hatırlatabilir. +Comment[uk]=Втягнути вікна до стільниці, щоб показати останню. Так вам простіше буде щось згадати. +Comment[x-test]=xxSuck windows into desktop to show the latter. This might remind you of something.xx +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-Depends= +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..35dc6c0 --- /dev/null +++ b/effects/fade/CMakeLists.txt @@ -0,0 +1,6 @@ +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) diff --git a/effects/fade/package/contents/code/main.js b/effects/fade/package/contents/code/main.js new file mode 100644 index 0000000..4f18cb2 --- /dev/null +++ b/effects/fade/package/contents/code/main.js @@ -0,0 +1,101 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2007 Philip Falkner + Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ + +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.deleted && effect.isGrabbed(w, Effect.WindowClosedGrabRole)) { + return false; + } else if (!w.deleted && effect.isGrabbed(w, Effect.WindowAddedGrabRole)) { + return false; + } + return w.onCurrentDesktop && !w.desktopWindow && !w.utility && !w.minimized; +} + +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 (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 (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.windowShown.connect(fadeInHandler); +effects.windowClosed.connect(fadeOutHandler); +effects.windowHidden.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..23f71a0 --- /dev/null +++ b/effects/fade/package/metadata.desktop @@ -0,0 +1,162 @@ +[Desktop Entry] +Name=Fade +Name[af]=Vervaag +Name[ar]=التلاشي +Name[be]=Павольнае знікненне +Name[bg]=Избледняване +Name[bn]=ফেড +Name[bs]=Utapanje +Name[ca]=Apagat gradual +Name[ca@valencia]=Apagat gradual +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]=Fade +Name[is]=Þynna út +Name[it]=Dissolvi +Name[ja]=フェード +Name[kk]=Біртіндеп +Name[km]=លេច​បន្តិច​ម្ដងៗ​ +Name[kn]=ಮಾಸು/ಮಸುಕುಗೊಳಿಸು +Name[ko]=페이드 +Name[lt]=Išnykimas +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]=Fade +Name[ro]=Decolorare +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[tg]=Плавное появление и исчезновение +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[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'encenguin o s'apaguin 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 jendela melesap lemah dan kuat ketika jendela 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 pamažu išnyksta/atsiranda jei juos prašoma parodyti arba jie slepiami +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]=Gładkie zanikanie i wyłanianie się okien przy ich otwieraniu i 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 afișate 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=Appearance +X-KDE-PluginInfo-Depends= +X-KDE-PluginInfo-License=GPL +X-KDE-PluginInfo-EnabledByDefault=true +X-KDE-Ordering=60 diff --git a/effects/fadedesktop/CMakeLists.txt b/effects/fadedesktop/CMakeLists.txt new file mode 100644 index 0000000..8926da4 --- /dev/null +++ b/effects/fadedesktop/CMakeLists.txt @@ -0,0 +1,6 @@ +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) diff --git a/effects/fadedesktop/package/contents/code/main.js b/effects/fadedesktop/package/contents/code/main.js new file mode 100644 index 0000000..d3ba74c --- /dev/null +++ b/effects/fadedesktop/package/contents/code/main.js @@ -0,0 +1,48 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2009 Lucas Murray + Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +var duration; +function loadConfig() { + duration = animationTime(250); +} +loadConfig(); +effect.configChanged.connect(function() { + loadConfig(); +}); +effects['desktopChanged(int,int)'].connect(function(oldDesktop, newDesktop) { + var stackingOrder = effects.stackingOrder; + for (var i=0; i + +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, see . +*********************************************************************/ + +#include "fallapart.h" +// KConfigSkeleton +#include "fallapartconfig.h" +#include +#include + +namespace KWin +{ + +bool FallApartEffect::supported() +{ + return effects->isOpenGLCompositing() && effects->animationsSupported(); +} + +FallApartEffect::FallApartEffect() +{ + initConfig(); + reconfigure(ReconfigureAll); + connect(effects, SIGNAL(windowClosed(KWin::EffectWindow*)), this, SLOT(slotWindowClosed(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowDeleted(KWin::EffectWindow*)), this, SLOT(slotWindowDeleted(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowDataChanged(KWin::EffectWindow*,int)), this, SLOT(slotWindowDataChanged(KWin::EffectWindow*,int))); +} + +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->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..b8d27a5 --- /dev/null +++ b/effects/fallapart/fallapart.h @@ -0,0 +1,67 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak + +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, see . +*********************************************************************/ + +#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(); + virtual void reconfigure(ReconfigureFlags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time); + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual void postPaintScreen(); + virtual bool isActive() const; + + 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..6915284 --- /dev/null +++ b/effects/flipswitch/CMakeLists.txt @@ -0,0 +1,25 @@ +####################################### +# Config +set(kwin_flipswitch_config_SRCS flipswitch_config.cpp) +ki18n_wrap_ui(kwin_flipswitch_config_SRCS flipswitch_config.ui) +qt5_add_dbus_interface(kwin_flipswitch_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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::Service + KF5::XmlGui +) + +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..c72b953 --- /dev/null +++ b/effects/flipswitch/flipswitch.cpp @@ -0,0 +1,988 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008, 2009 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "flipswitch.h" +// KConfigSkeleton +#include "flipswitchconfig.h" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include + +namespace KWin +{ + +FlipSwitchEffect::FlipSwitchEffect() + : m_selectedWindow(nullptr) + , m_currentAnimationShape(QTimeLine::EaseInOutCurve) + , m_active(false) + , m_start(false) + , m_stop(false) + , m_animation(false) + , m_hasKeyboardGrab(false) + , m_captionFrame(NULL) +{ + 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, SIGNAL(triggered(bool)), this, SLOT(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, SIGNAL(triggered(bool)), this, SLOT(toggleActiveAllDesktops())); + connect(KGlobalAccel::self(), &KGlobalAccel::globalShortcutChanged, this, &FlipSwitchEffect::globalShortcutChanged); + connect(effects, SIGNAL(windowAdded(KWin::EffectWindow*)), this, SLOT(slotWindowAdded(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowClosed(KWin::EffectWindow*)), this, SLOT(slotWindowClosed(KWin::EffectWindow*))); + connect(effects, SIGNAL(tabBoxAdded(int)), this, SLOT(slotTabBoxAdded(int))); + connect(effects, SIGNAL(tabBoxClosed()), this, SLOT(slotTabBoxClosed())); + connect(effects, SIGNAL(tabBoxUpdated()), this, SLOT(slotTabBoxUpdated())); + connect(effects, SIGNAL(tabBoxKeyEvent(QKeyEvent*)), this, SLOT(slotTabBoxKeyEvent(QKeyEvent*))); +} + +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, QRegion region, 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 = NULL; + 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 = NULL; + 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_currentAnimationShape = QTimeLine::EaseOutCurve; + m_timeLine.setCurveShape(m_currentAnimationShape); + } else { + m_currentAnimationShape = QTimeLine::LinearCurve; + m_timeLine.setCurveShape(m_currentAnimationShape); + } + } + effects->addRepaintFull(); + } + if (m_stop && m_startStopTimeLine.currentValue() == 0.0f) { + m_stop = false; + m_active = false; + m_captionFrame->free(); + effects->setActiveFullScreenEffect(0); + 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_currentAnimationShape = QTimeLine::LinearCurve; + else + m_currentAnimationShape = QTimeLine::EaseOutCurve; + } else { + m_currentAnimationShape = QTimeLine::LinearCurve; + } + m_timeLine.setCurveShape(m_currentAnimationShape); + } + } + 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); + if (!w->isCurrentTab()) + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_TAB_GROUP); + } 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 != NULL) { + 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 = 0; + 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.setCurveShape(QTimeLine::EaseInOutCurve); + 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.setCurveShape(QTimeLine::EaseOutCurve); + if (m_scheduledDirections.count() == 1) { + if (m_currentAnimationShape == QTimeLine::EaseInOutCurve) + m_currentAnimationShape = QTimeLine::EaseInCurve; + else if (m_currentAnimationShape == QTimeLine::EaseOutCurve) + m_currentAnimationShape = QTimeLine::LinearCurve; + m_timeLine.setCurveShape(m_currentAnimationShape); + } + } else + m_startStopTimeLine.setCurveShape(QTimeLine::EaseInOutCurve); + 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.setCurveShape(QTimeLine::EaseInCurve); + } + if (!m_animation && !m_start) { + m_animation = true; + m_scheduledDirections.enqueue(direction); + distance--; + // reset shape just to make sure + m_currentAnimationShape = QTimeLine::EaseInOutCurve; + m_timeLine.setCurveShape(m_currentAnimationShape); + } + 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) { + QTimeLine::CurveShape newShape = QTimeLine::EaseInOutCurve; + switch(m_currentAnimationShape) { + case QTimeLine::EaseInOutCurve: + newShape = QTimeLine::EaseInCurve; + break; + case QTimeLine::EaseOutCurve: + newShape = QTimeLine::LinearCurve; + break; + default: + newShape = m_currentAnimationShape; + } + if (newShape != m_currentAnimationShape) { + m_currentAnimationShape = newShape; + m_timeLine.setCurveShape(m_currentAnimationShape); + } + } +} + +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, 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::MidButton: + 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..add05b4 --- /dev/null +++ b/effects/flipswitch/flipswitch.h @@ -0,0 +1,165 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008, 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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(); + + virtual void reconfigure(ReconfigureFlags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual void postPaintScreen(); + virtual void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time); + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual void grabbedKeyboardEvent(QKeyEvent* e); + virtual void windowInputMouseEvent(QEvent* e); + virtual bool isActive() const; + + 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, 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; + QTimeLine::CurveShape m_currentAnimationShape; + 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..f53f5cc --- /dev/null +++ b/effects/flipswitch/flipswitch_config.cpp @@ -0,0 +1,97 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008, 2009 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..f597a35 --- /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[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]=Flip Switch +Name[is]=Flettirofi +Name[it]=Scambiafinestre a pila +Name[ja]=フリップスイッチ +Name[kk]=Ақтарып ауыстырғыш +Name[km]=ប្ដូរ​ការ​ត្រឡប់​ +Name[kn]=ಬದಲಾವಣೆ ಗುಂಡಿ (ಫ್ಲಿಪ್ ಸ್ವಿಚ್) +Name[ko]=플립 전환기 +Name[lt]=Kartotekos imitacijos keitiklis +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]=Comutare 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[tg]=Перелистывание +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..2c1e0fa --- /dev/null +++ b/effects/flipswitch/flipswitch_config.h @@ -0,0 +1,57 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008, 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + ~FlipSwitchEffectConfig(); + +public Q_SLOTS: + virtual void save(); + +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/CMakeLists.txt b/effects/frozenapp/CMakeLists.txt new file mode 100644 index 0000000..1242620 --- /dev/null +++ b/effects/frozenapp/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(package) diff --git a/effects/frozenapp/package/CMakeLists.txt b/effects/frozenapp/package/CMakeLists.txt new file mode 100644 index 0000000..e426a84 --- /dev/null +++ b/effects/frozenapp/package/CMakeLists.txt @@ -0,0 +1,6 @@ +install(DIRECTORY contents DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_frozenapp) +install(FILES metadata.desktop DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_frozenapp) + +install(FILES metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_effect_frozenapp.desktop) diff --git a/effects/frozenapp/package/contents/code/main.js b/effects/frozenapp/package/contents/code/main.js new file mode 100644 index 0000000..7557bc4 --- /dev/null +++ b/effects/frozenapp/package/contents/code/main.js @@ -0,0 +1,140 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2017 Kai Uwe Broulik + +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, see . +*********************************************************************/ +/*global effect, effects, animate, animationTime, Effect*/ +var frozenAppEffect = { + inDuration: animationTime(1500), + outDuration: animationTime(250), + loadConfig: function () { + "use strict"; + frozenAppEffect.inDuration = animationTime(1500); + frozenAppEffect.outDuration = animationTime(250); + }, + windowAdded: function (window) { + "use strict"; + if (!window || !window.unresponsive) { + return; + } + frozenAppEffect.windowBecameUnresponsive(window); + }, + windowBecameUnresponsive: function (window) { + "use strict"; + if (window.unresponsiveAnimation) { + return; + } + frozenAppEffect.startAnimation(window, frozenAppEffect.inDuration); + }, + startAnimation: function (window, duration) { + "use strict"; + 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) { + "use strict"; + frozenAppEffect.cancelAnimation(window); + if (!window.unresponsive) { + return; + } + frozenAppEffect.windowBecameResponsive(window); + }, + windowBecameResponsive: function (window) { + "use strict"; + 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) { + "use strict"; + if (window.unresponsiveAnimation) { + cancel(window.unresponsiveAnimation); + window.unresponsiveAnimation = undefined; + } + }, + desktopChanged: function () { + "use strict"; + + 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) { + "use strict"; + if (window.unresponsive) { + frozenAppEffect.windowBecameUnresponsive(window); + } else { + frozenAppEffect.windowBecameResponsive(window); + } + }, + restartAnimation: function (window) { + "use strict"; + if (!window || !window.unresponsive) { + return; + } + frozenAppEffect.startAnimation(window, 1); + }, + init: function () { + "use strict"; + + 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..57aafbe --- /dev/null +++ b/effects/frozenapp/package/metadata.desktop @@ -0,0 +1,90 @@ +[Desktop Entry] +Name=Desaturate Unresponsive Applications +Name[ca]=Dessatura les aplicacions que no responen +Name[ca@valencia]=Dessatura les aplicacions que no responen +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[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 dos aplicativos que non responden +Name[he]=מחשיך יישומים שאינם מגיבים +Name[hu]=Nem válaszoló alkalmazások színtelenítése +Name[id]=Desaturate Unresponsive Applications +Name[it]=Desatura le applicazioni che non rispondono +Name[ko]=응답 없는 프로그램을 무채색으로 전환 +Name[nl]=Verzadiging van niet responsieve toepassingen verminderen +Name[nn]=Fjern fargemetting på ikkje-responsive program +Name[pl]=Odbarw nieodpowiadające aplikacje +Name[pt]=Reduzir a Saturação das Aplicações Bloqueadas +Name[pt_BR]=Reduzir saturação de aplicativos que não respondem +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[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[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 aplicativos que non responden (que queradon conxelados). +Comment[he]=מחשיך חלונות של יישומים שאינם מגיבים (תקועים) +Comment[hu]=Színteleníti a nem válaszoló, lefagyott alkalmazások ablakait +Comment[id]=Jendela desaturasi dari aplikasi yang tidak responsif (beku) +Comment[it]=Desatura le finestre delle applicazione che non rispondono (bloccate) +Comment[ko]=응답 없는 프로그램 창을 무채색으로 전환 +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]=Odbarw 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[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-Depends= +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/glide/CMakeLists.txt b/effects/glide/CMakeLists.txt new file mode 100644 index 0000000..4ae578c --- /dev/null +++ b/effects/glide/CMakeLists.txt @@ -0,0 +1,25 @@ +####################################### +# Config +set(kwin_glide_config_SRCS glide_config.cpp) +ki18n_wrap_ui(kwin_glide_config_SRCS glide_config.ui) +qt5_add_dbus_interface(kwin_glide_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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 + Qt5::DBus + KF5::ConfigWidgets + KF5::I18n + KF5::Service +) + +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..6962d13 --- /dev/null +++ b/effects/glide/glide.cpp @@ -0,0 +1,324 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Philip Falkner +Copyright (C) 2009 Martin Gräßlin +Copyright (C) 2010 Alexandre Pereira +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +// 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")) { + return w->hasDecoration(); + } + + if (s_blacklist.contains(w->windowClass())) { + return false; + } + + if (w->hasDecoration()) { + return true; + } + + if (!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..5dbc81c --- /dev/null +++ b/effects/glide/glide.h @@ -0,0 +1,156 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Philip Falkner +Copyright (C) 2009 Martin Gräßlin +Copyright (C) 2010 Alexandre Pereira +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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..a151b9b --- /dev/null +++ b/effects/glide/glide_config.cpp @@ -0,0 +1,61 @@ +/* + * Copyright © 2010 Fredrik Höglund + * Copyright (C) 2010 Alexandre Pereira + * + * 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; see the file COPYING. if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#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..e801c06 --- /dev/null +++ b/effects/glide/glide_config.desktop @@ -0,0 +1,68 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_glide_config +X-KDE-ParentComponents=glide + +Name=Glide +Name[ar]=الطيران +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]=Glide +Name[is]=Svífa +Name[it]=Plana +Name[ja]=グライド +Name[kk]=Сырғанау +Name[km]=សំកាំង +Name[kn]=ಜಾರು +Name[ko]=글라이드 +Name[lt]=Sklendimas +Name[lv]=Slīdēt +Name[mr]=घसरणे +Name[nb]=Skyv +Name[nds]=Glieden +Name[nl]=Schuiven +Name[nn]=Skliding +Name[pa]=ਗਲਾਈਡ +Name[pl]=Szybowanie +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..5a5f0a7 --- /dev/null +++ b/effects/glide/glide_config.h @@ -0,0 +1,47 @@ +/* + * Copyright © 2010 Fredrik Höglund + * Copyright (C) 2010 Alexandre Pereira + * + * 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; see the file COPYING. if not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#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..c459335 --- /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..355220c --- /dev/null +++ b/effects/highlightwindow/highlightwindow.cpp @@ -0,0 +1,309 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +#include "highlightwindow.h" + +namespace KWin +{ + +HighlightWindowEffect::HighlightWindowEffect() + : m_finishing(false) + , m_fadeDuration(float(animationTime(150))) + , m_monitorWindow(NULL) +{ + m_atom = effects->announceSupportProperty("_KDE_WINDOW_HIGHLIGHT", this); + connect(effects, SIGNAL(windowAdded(KWin::EffectWindow*)), this, SLOT(slotWindowAdded(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowClosed(KWin::EffectWindow*)), this, SLOT(slotWindowClosed(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowDeleted(KWin::EffectWindow*)), this, SLOT(slotWindowDeleted(KWin::EffectWindow*))); + connect(effects, SIGNAL(propertyNotify(KWin::EffectWindow*,long)), this, SLOT(slotPropertyNotify(KWin::EffectWindow*,long))); + 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->isCurrentTab() || !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.insertMulti(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->isCurrentTab()) + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_TAB_GROUP); + 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.insertMulti(w, isInitiallyHidden(w) ? 0.0 : 1.0); + if (!m_highlightedWindows.isEmpty()) + m_highlightedWindows.at(0)->addRepaintFull(); + } +} + +void HighlightWindowEffect::finishHighlighting() +{ + m_finishing = true; + m_monitorWindow = NULL; + 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..2de9b3d --- /dev/null +++ b/effects/highlightwindow/highlightwindow.h @@ -0,0 +1,87 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +#ifndef KWIN_HIGHLIGHTWINDOW_H +#define KWIN_HIGHLIGHTWINDOW_H + +#include + +namespace KWin +{ + +class HighlightWindowEffect + : public Effect +{ + Q_OBJECT +public: + HighlightWindowEffect(); + virtual ~HighlightWindowEffect(); + + virtual void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time); + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual bool isActive() const; + + 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 = NULL); + +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..c81978d --- /dev/null +++ b/effects/invert/CMakeLists.txt @@ -0,0 +1,26 @@ +####################################### +# Effect + +####################################### +# Config +set(kwin_invert_config_SRCS invert_config.cpp) +qt5_add_dbus_interface(kwin_invert_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) + +add_library(kwin_invert_config MODULE ${kwin_invert_config_SRCS}) + +target_link_libraries(kwin_invert_config + KF5::ConfigWidgets + KF5::GlobalAccel + KF5::I18n + KF5::Service + KF5::XmlGui +) + +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..58c32ca --- /dev/null +++ b/effects/invert/invert.cpp @@ -0,0 +1,151 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2008 Lucas Murray + +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, see . +*********************************************************************/ + +#include "invert.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace KWin +{ + +InvertEffect::InvertEffect() + : m_inited(false), + m_valid(true), + m_shader(NULL), + 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, SIGNAL(triggered(bool)), this, SLOT(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, SIGNAL(triggered(bool)), this, SLOT(toggleWindow())); + + connect(effects, SIGNAL(windowClosed(KWin::EffectWindow*)), this, SLOT(slotWindowClosed(KWin::EffectWindow*))); +} + +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, QRegion region, 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, QRegion region, 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..65d2e8d --- /dev/null +++ b/effects/invert/invert.h @@ -0,0 +1,75 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2008 Lucas Murray + +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, see . +*********************************************************************/ + +#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(); + + virtual void drawWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual void paintEffectFrame(KWin::EffectFrame* frame, QRegion region, double opacity, double frameOpacity); + virtual bool isActive() const; + virtual bool provides(Feature); + + 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..5888fb7 --- /dev/null +++ b/effects/invert/invert_config.cpp @@ -0,0 +1,107 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks + +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, see . +*********************************************************************/ + +#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, SIGNAL(keyChange()), this, SLOT(changed())); + 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..7f16f03 --- /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[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]=Invert +Name[is]=Umhverfa +Name[it]=Inverti +Name[ja]=色調反転 +Name[kk]=Терістеу +Name[km]=ដាក់​បញ្ច្រាស +Name[kn]=ವಿಲೋಮಗೊಳಿಸು (ಇನ್ವರ್ಟ್) +Name[ko]=반전 +Name[ku]=Vajî +Name[lt]=Išvertimas +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[tg]=Табдилдиҳӣ +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..1fab78c --- /dev/null +++ b/effects/invert/invert_config.h @@ -0,0 +1,49 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + ~InvertEffectConfig(); + +public Q_SLOTS: + virtual void save(); + virtual void load(); + virtual void defaults(); + +private: + KShortcutsEditor* mShortcutEditor; +}; + +} // namespace + +#endif diff --git a/effects/kscreen/CMakeLists.txt b/effects/kscreen/CMakeLists.txt new file mode 100644 index 0000000..46e97f8 --- /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..d377fab --- /dev/null +++ b/effects/kscreen/kscreen.cpp @@ -0,0 +1,192 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +// 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, SIGNAL(propertyNotify(KWin::EffectWindow*,long)), SLOT(propertyNotify(KWin::EffectWindow*,long))); + 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..2f9dad1 --- /dev/null +++ b/effects/kscreen/kscreen.h @@ -0,0 +1,66 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_KSCREEN_H +#define KWIN_KSCREEN_H + +#include + +namespace KWin +{ + +class KscreenEffect : public Effect +{ + Q_OBJECT + +public: + KscreenEffect(); + virtual ~KscreenEffect(); + + virtual void prePaintScreen(ScreenPrePaintData &data, int time); + virtual void postPaintScreen(); + virtual void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time); + virtual void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data); + + void reconfigure(ReconfigureFlags flags); + virtual bool isActive() const; + + 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..24029ca --- /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[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 del 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[tg]=Воситаҳои 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..d338c90 --- /dev/null +++ b/effects/logging.cpp @@ -0,0 +1,21 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#include +Q_LOGGING_CATEGORY(KWINEFFECTS, "kwineffects", QtCriticalMsg) diff --git a/effects/login/CMakeLists.txt b/effects/login/CMakeLists.txt new file mode 100644 index 0000000..1242620 --- /dev/null +++ b/effects/login/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(package) diff --git a/effects/login/package/CMakeLists.txt b/effects/login/package/CMakeLists.txt new file mode 100644 index 0000000..5e8c10c --- /dev/null +++ b/effects/login/package/CMakeLists.txt @@ -0,0 +1,6 @@ +install(DIRECTORY contents DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_login) +install(FILES metadata.desktop DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_login) + +install(FILES metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_effect_login.desktop) diff --git a/effects/login/package/contents/code/main.js b/effects/login/package/contents/code/main.js new file mode 100644 index 0000000..09e008a --- /dev/null +++ b/effects/login/package/contents/code/main.js @@ -0,0 +1,94 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2007 Lubos Lunak + Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +/*global effect, effects, animate, animationTime, Effect*/ +var loginEffect = { + duration: animationTime(2000), + isFadeToBlack: false, + loadConfig: function () { + "use strict"; + loginEffect.isFadeToBlack = effect.readConfig("FadeToBlack", false); + }, + isLoginSplash: function (window) { + "use strict"; + 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) { + "use strict"; + animate({ + window: window, + duration: loginEffect.duration, + type: Effect.Opacity, + from: 1.0, + to: 0.0 + }); + }, + fadeToBlack: function (window) { + "use strict"; + 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) { + "use strict"; + if (!loginEffect.isLoginSplash(window)) { + return; + } + if (loginEffect.isFadeToBlack === true) { + loginEffect.fadeToBlack(window); + } else { + loginEffect.fadeOut(window); + } + }, + init: function () { + "use strict"; + 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..495fbb3 --- /dev/null +++ b/effects/login/package/metadata.desktop @@ -0,0 +1,171 @@ +[Desktop Entry] +Name=Login +Name[af]=Aanteken +Name[ar]=ولوج +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]=Prisijungti +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[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 log masuk +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]=Sklandžiai išryškina darbalaukį prisijungiant +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-Depends= +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/CMakeLists.txt b/effects/logout/CMakeLists.txt new file mode 100644 index 0000000..1242620 --- /dev/null +++ b/effects/logout/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(package) diff --git a/effects/logout/package/CMakeLists.txt b/effects/logout/package/CMakeLists.txt new file mode 100644 index 0000000..885e3db --- /dev/null +++ b/effects/logout/package/CMakeLists.txt @@ -0,0 +1,6 @@ +install(DIRECTORY contents DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_logout) +install(FILES metadata.desktop DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_logout) + +install(FILES metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_effect_logout.desktop) diff --git a/effects/logout/package/contents/code/main.js b/effects/logout/package/contents/code/main.js new file mode 100644 index 0000000..ca916d9 --- /dev/null +++ b/effects/logout/package/contents/code/main.js @@ -0,0 +1,87 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2007 Lubos Lunak + Copyright (C) 2013 Martin Gräßlin + Copyright (C) 2017 Marco Martin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +/*global effect, effects, animate, animationTime, Effect*/ +var logoutEffect = { + inDuration: animationTime(800), + outDuration: animationTime(400), + loadConfig: function () { + "use strict"; + logoutEffect.inDuration = animationTime(800); + logoutEffect.outDuration = animationTime(400); + }, + isLogoutWindow: function (window) { + "use strict"; + if (window.windowClass === "ksmserver ksmserver") { + return true; + } + if (window.windowClass === "ksmserver-logout-greeter ksmserver-logout-greeter") { + return true; + } + return false; + }, + opened: function (window) { + "use strict"; + 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) { + "use strict"; + 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 () { + "use strict"; + logoutEffect.loadConfig(); + effects.windowAdded.connect(logoutEffect.opened); + effects.windowShown.connect(logoutEffect.opened); + effects.windowClosed.connect(logoutEffect.closed); + effects.windowHidden.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..cb13339 --- /dev/null +++ b/effects/logout/package/metadata.desktop @@ -0,0 +1,77 @@ +[Desktop Entry] +Name=Logout +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[eu]=Saio-ixtea +Name[fi]=Kirjaudu ulos +Name[fr]=Déconnexion +Name[gl]=Saír +Name[hu]=Kijelentkezés +Name[id]=Logout +Name[it]=Uscita +Name[ko]=로그아웃 +Name[nl]=Afmelden +Name[nn]=Logg ut +Name[pl]=Wylogowywanie +Name[pt]=Encerrar +Name[pt_BR]=Encerrar sessão +Name[ru]=Завершение работы +Name[sk]=Odhlásiť sa +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[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[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[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[id]=Melesap secara halus ke layar logout +Comment[it]=Dissolvenza graduale alla schermata di uscita +Comment[ko]=로그아웃 화면으로 부드럽게 전환합니다 +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[ru]=Плавное появление экрана завершения работы +Comment[sk]=Plynule zobrazí plochu pri odhlásení +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,KCModule +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-Depends= +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_logout +X-KDE-Library=kcm_kwin4_genericscripted +X-KDE-ParentComponents=kwin4_effect_logout +X-KWin-Config-TranslationDomain=kwin_effects diff --git a/effects/lookingglass/CMakeLists.txt b/effects/lookingglass/CMakeLists.txt new file mode 100644 index 0000000..4e5c3ef --- /dev/null +++ b/effects/lookingglass/CMakeLists.txt @@ -0,0 +1,28 @@ +####################################### +# Effect + +####################################### +# Config +set(kwin_lookingglass_config_SRCS lookingglass_config.cpp) +ki18n_wrap_ui(kwin_lookingglass_config_SRCS lookingglass_config.ui) +qt5_add_dbus_interface(kwin_lookingglass_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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::Service + KF5::XmlGui +) + +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..f275cd8 --- /dev/null +++ b/effects/lookingglass/lookingglass.cpp @@ -0,0 +1,257 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +#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(NULL) + , m_fbo(NULL) + , m_vbo(NULL) + , m_shader(NULL) + , 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, SIGNAL(mouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers)), + this, SLOT(slotMouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers))); + 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, QRegion region, ScreenPaintData &data) +{ + // Call the next effect. + effects->paintScreen(mask, region, data); + if (m_valid && m_enabled) { + // Disable render texture + GLRenderTarget* target = GLRenderTarget::popRenderTarget(); + 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..2252a70 --- /dev/null +++ b/effects/lookingglass/lookingglass.h @@ -0,0 +1,83 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +#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(); + virtual ~LookingGlassEffect(); + + virtual void reconfigure(ReconfigureFlags); + + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + void paintScreen(int mask, QRegion region, ScreenPaintData &data) override; + virtual bool isActive() const; + + 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..7b2de55 --- /dev/null +++ b/effects/lookingglass/lookingglass_config.cpp @@ -0,0 +1,119 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +#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, SIGNAL(keyChange()), this, SLOT(changed())); + + // 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..77dfd0f --- /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[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]=Looking Glass +Name[is]=Spegilgler +Name[it]=Specchio +Name[ja]=拡大鏡 +Name[kk]=Лупа +Name[km]=កញ្ចក់​មើល +Name[kn]=ಲುಕಿಂಗ್ ಗ್ಲಾಸ್ +Name[ko]=들여다보는 돋보기 +Name[lt]=Didinamasis stiklas +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]=Binoclu +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[tg]=Увеличительное стекло +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..ef81612 --- /dev/null +++ b/effects/lookingglass/lookingglass_config.h @@ -0,0 +1,57 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + virtual ~LookingGlassEffectConfig(); + + virtual void save(); + virtual void defaults(); + +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..c1dc011 --- /dev/null +++ b/effects/magiclamp/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_magiclamp_config_SRCS magiclamp_config.cpp) +ki18n_wrap_ui(kwin_magiclamp_config_SRCS magiclamp_config.ui) +qt5_add_dbus_interface(kwin_magiclamp_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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 + Qt5::DBus + KF5::ConfigWidgets + KF5::I18n + KF5::Service +) + +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..e382156 --- /dev/null +++ b/effects/magiclamp/magiclamp.cpp @@ -0,0 +1,374 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008 Martin Gräßlin + +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, see . +*********************************************************************/ + +// based on minimize animation by Rivo Laks + +#include "magiclamp.h" +// KConfigSkeleton +#include "magiclampconfig.h" + +namespace KWin +{ + +MagicLampEffect::MagicLampEffect() +{ + initConfig(); + reconfigure(ReconfigureAll); + connect(effects, SIGNAL(windowDeleted(KWin::EffectWindow*)), this, SLOT(slotWindowDeleted(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowMinimized(KWin::EffectWindow*)), this, SLOT(slotWindowMinimized(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowUnminimized(KWin::EffectWindow*)), this, SLOT(slotWindowUnminimized(KWin::EffectWindow*))); +} + +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 = NULL; + 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.y()) + position = Top; + else + position = Bottom; + } else { + // vertical panel + if (panel->x() == panelScreen.x()) + 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..0bd900b --- /dev/null +++ b/effects/magiclamp/magiclamp.h @@ -0,0 +1,69 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef KWIN_MAGICLAMP_H +#define KWIN_MAGICLAMP_H + +#include + +namespace KWin +{ + +class MagicLampEffect + : public Effect +{ + Q_OBJECT + +public: + MagicLampEffect(); + + virtual void reconfigure(ReconfigureFlags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time); + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual void postPaintScreen(); + virtual bool isActive() const; + + 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..1972116 --- /dev/null +++ b/effects/magiclamp/magiclamp_config.cpp @@ -0,0 +1,71 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..07b4858 --- /dev/null +++ b/effects/magiclamp/magiclamp_config.desktop @@ -0,0 +1,79 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_magiclamp_config +X-KDE-ParentComponents=magiclamp + +Name=Magic Lamp +Name[ar]=المصباح السحري +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]=Magic Lamp +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[tg]=Contact - чароғ +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..4f07dd8 --- /dev/null +++ b/effects/magiclamp/magiclamp_config.h @@ -0,0 +1,54 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + +public Q_SLOTS: + virtual void save(); + +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..9aa1dca --- /dev/null +++ b/effects/magnifier/CMakeLists.txt @@ -0,0 +1,25 @@ +####################################### +# Config +set(kwin_magnifier_config_SRCS magnifier_config.cpp) +ki18n_wrap_ui(kwin_magnifier_config_SRCS magnifier_config.ui) +qt5_add_dbus_interface(kwin_magnifier_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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::Service + KF5::XmlGui +) + +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..22aa7fb --- /dev/null +++ b/effects/magnifier/magnifier.cpp @@ -0,0 +1,342 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak +Copyright (C) 2007 Christian Nitschkowski +Copyright (C) 2011 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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(0) + , m_fbo(0) +#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, SIGNAL(mouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers)), + this, SLOT(slotMouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers))); + 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 = NULL; + m_texture = NULL; + 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, QRegion region, 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(), NULL); + + 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, NULL); + 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, NULL); + 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 = NULL; + m_texture = NULL; + 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..14345bc --- /dev/null +++ b/effects/magnifier/magnifier.h @@ -0,0 +1,82 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak +Copyright (C) 2011 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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(); + virtual ~MagnifierEffect(); + virtual void reconfigure(ReconfigureFlags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual void postPaintScreen(); + virtual bool isActive() const; + 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..61f4ae9 --- /dev/null +++ b/effects/magnifier/magnifier_config.cpp @@ -0,0 +1,120 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +#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, SIGNAL(keyChange()), this, SLOT(changed())); + + // 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..a7c4e77 --- /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[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]=Magnifier +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]=Približ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[tg]=Увеличитель +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..f21652a --- /dev/null +++ b/effects/magnifier/magnifier_config.h @@ -0,0 +1,57 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + virtual ~MagnifierEffectConfig(); + + virtual void save(); + virtual void defaults(); + +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/CMakeLists.txt b/effects/maximize/CMakeLists.txt new file mode 100644 index 0000000..28f40b3 --- /dev/null +++ b/effects/maximize/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory( package ) diff --git a/effects/maximize/package/CMakeLists.txt b/effects/maximize/package/CMakeLists.txt new file mode 100644 index 0000000..e1262fa --- /dev/null +++ b/effects/maximize/package/CMakeLists.txt @@ -0,0 +1,6 @@ +install(DIRECTORY contents DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_maximize) +install(FILES metadata.desktop DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_maximize) + +install(FILES metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_effect_maximize.desktop) diff --git a/effects/maximize/package/contents/code/maximize.js b/effects/maximize/package/contents/code/maximize.js new file mode 100644 index 0000000..61547ee --- /dev/null +++ b/effects/maximize/package/contents/code/maximize.js @@ -0,0 +1,103 @@ +/******************************************************************** + This file is part of the KDE project. + + Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +/*global effect, effects, animate, animationTime, Effect*/ +var maximizeEffect = { + duration: animationTime(250), + loadConfig: function () { + "use strict"; + maximizeEffect.duration = animationTime(250); + }, + maximizeChanged: function (window) { + "use strict"; + 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) { + "use strict"; + 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 () { + "use strict"; + effect.configChanged.connect(maximizeEffect.loadConfig); + effects.windowGeometryShapeChanged.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..45d0c62 --- /dev/null +++ b/effects/maximize/package/metadata.desktop @@ -0,0 +1,113 @@ +[Desktop Entry] +Comment=Animation for a window going to maximize/restore from maximize +Comment[bs]=Animacija za prozor koji ide u maksimiziranje/vraćanje iz maksimiziranja +Comment[ca]=Animació per 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 jendela yang maksimalkan/kembalikan dari maksimalkan +Comment[it]=Animazione per una finestra massimizzata o ripristinata dalla massimizzazione +Comment[kk]=Терезені кең жаю/қалпына қайтаруды анимациясы +Comment[ko]=최대화된 창이 최소화되거나 복원될 때 사용할 애니메이션 +Comment[lt]=Išdidinti/Atstatyti +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]=Efekt skalowania okien przy maksymalizowaniu i powrocie z niego +Comment[pt]=Animação para um 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[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]=Maximize +Name[is]=Hámarka +Name[it]=Massimizzazione +Name[ja]=最大化 +Name[kk]=Кең жаю +Name[ko]=최대화 +Name[lt]=Išdidinti +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=http://files.kde.org/plasma/kwin/effect-videos/maximize.ogv diff --git a/effects/minimizeanimation/CMakeLists.txt b/effects/minimizeanimation/CMakeLists.txt new file mode 100644 index 0000000..043a527 --- /dev/null +++ b/effects/minimizeanimation/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# Effect + +# Source files +set( kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + minimizeanimation/minimizeanimation.cpp + ) diff --git a/effects/minimizeanimation/minimizeanimation.cpp b/effects/minimizeanimation/minimizeanimation.cpp new file mode 100644 index 0000000..dbb9c6c --- /dev/null +++ b/effects/minimizeanimation/minimizeanimation.cpp @@ -0,0 +1,165 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks + +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, see . +*********************************************************************/ + +#include "minimizeanimation.h" + +#include + +namespace KWin +{ + +MinimizeAnimationEffect::MinimizeAnimationEffect() +{ + reconfigure(ReconfigureAll); + + connect(effects, &EffectsHandler::windowDeleted, this, &MinimizeAnimationEffect::windowDeleted); + connect(effects, &EffectsHandler::windowMinimized, this, &MinimizeAnimationEffect::windowMinimized); + connect(effects, &EffectsHandler::windowUnminimized, this, &MinimizeAnimationEffect::windowUnminimized); +} + +void MinimizeAnimationEffect::reconfigure(ReconfigureFlags flags) +{ + Q_UNUSED(flags) + + m_duration = std::chrono::milliseconds(static_cast(animationTime(250))); +} + +bool MinimizeAnimationEffect::supported() +{ + return effects->animationsSupported(); +} + +void MinimizeAnimationEffect::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 MinimizeAnimationEffect::prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time) +{ + if (m_animations.contains(w)) { + data.setTransformed(); + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_MINIMIZE); + } + + effects->prePaintWindow(w, data, time); +} + +void MinimizeAnimationEffect::paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) +{ + const 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(); + // If there's no icon geometry, minimize to the center of the screen + if (!icon.isValid()) { + icon = QRect(effects->virtualScreenGeometry().center(), QSize(0, 0)); + } + + data *= QVector2D(interpolate(1.0, icon.width() / (double)geo.width(), progress), + interpolate(1.0, icon.height() / (double)geo.height(), progress)); + data.setXTranslation(interpolate(data.xTranslation(), icon.x() - geo.x(), progress)); + data.setYTranslation(interpolate(data.yTranslation(), icon.y() - geo.y(), progress)); + data.multiplyOpacity(interpolate(1.0, 0.1, progress)); + } + + effects->paintWindow(w, mask, region, data); +} + +void MinimizeAnimationEffect::postPaintScreen() +{ + auto animationIt = m_animations.begin(); + while (animationIt != m_animations.end()) { + if ((*animationIt).done()) { + animationIt = m_animations.erase(animationIt); + } else { + ++animationIt; + } + } + + effects->addRepaintFull(); + + effects->postPaintScreen(); +} + +void MinimizeAnimationEffect::windowDeleted(EffectWindow *w) +{ + m_animations.remove(w); +} + +void MinimizeAnimationEffect::windowMinimized(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::InOutSine); + } + + effects->addRepaintFull(); +} + +void MinimizeAnimationEffect::windowUnminimized(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::InOutSine); + } + + effects->addRepaintFull(); +} + +bool MinimizeAnimationEffect::isActive() const +{ + return !m_animations.isEmpty(); +} + +} // namespace + diff --git a/effects/minimizeanimation/minimizeanimation.h b/effects/minimizeanimation/minimizeanimation.h new file mode 100644 index 0000000..4549bc7 --- /dev/null +++ b/effects/minimizeanimation/minimizeanimation.h @@ -0,0 +1,66 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks + +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, see . +*********************************************************************/ + +#ifndef KWIN_MINIMIZEANIMATION_H +#define KWIN_MINIMIZEANIMATION_H + +// Include with base class for effects. +#include + +namespace KWin +{ + +/** + * Animates minimize/unminimize + **/ +class MinimizeAnimationEffect : public Effect +{ + Q_OBJECT + +public: + MinimizeAnimationEffect(); + + 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 { + return 50; + } + + static bool supported(); + +private Q_SLOTS: + void windowDeleted(EffectWindow *w); + void windowMinimized(EffectWindow *w); + void windowUnminimized(EffectWindow *w); + +private: + std::chrono::milliseconds m_duration; + QHash m_animations; +}; + +} // namespace + +#endif diff --git a/effects/morphingpopups/CMakeLists.txt b/effects/morphingpopups/CMakeLists.txt new file mode 100644 index 0000000..28f40b3 --- /dev/null +++ b/effects/morphingpopups/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory( package ) diff --git a/effects/morphingpopups/package/CMakeLists.txt b/effects/morphingpopups/package/CMakeLists.txt new file mode 100644 index 0000000..1a804f5 --- /dev/null +++ b/effects/morphingpopups/package/CMakeLists.txt @@ -0,0 +1,6 @@ +install(DIRECTORY contents DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_morphingpopups) +install(FILES metadata.desktop DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_morphingpopups) + +install(FILES metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_effect_morphingpopups.desktop) diff --git a/effects/morphingpopups/package/contents/code/morphingpopups.js b/effects/morphingpopups/package/contents/code/morphingpopups.js new file mode 100644 index 0000000..16040dc --- /dev/null +++ b/effects/morphingpopups/package/contents/code/morphingpopups.js @@ -0,0 +1,140 @@ +/******************************************************************** + This file is part of the KDE project. + + Copyright (C) 2012 Martin Gräßlin + Copyright (C) 2016 Marco Martin + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*********************************************************************/ +/*global effect, effects, animate, animationTime, Effect*/ +var morphingEffect = { + duration: animationTime(150), + loadConfig: function () { + "use strict"; + morphingEffect.duration = animationTime(150); + }, + + geometryChange: function (window, oldGeometry) { + "use strict"; + + //only tooltips and notifications + if (!window.tooltip && !window.notification) { + 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; + } + + //WindowForceBackgroundContrastRole + window.setData(7, true); + //WindowForceBlurRole + window.setData(5, 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 () { + "use strict"; + effect.configChanged.connect(morphingEffect.loadConfig); + effects.windowGeometryShapeChanged.connect(morphingEffect.geometryChange); + } +}; +morphingEffect.init(); diff --git a/effects/morphingpopups/package/metadata.desktop b/effects/morphingpopups/package/metadata.desktop new file mode 100644 index 0000000..5499871 --- /dev/null +++ b/effects/morphingpopups/package/metadata.desktop @@ -0,0 +1,90 @@ +[Desktop Entry] +Comment=Cross fade animation when Tooltips or Notifications change their geometry +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 Tooltips atau Notifications berubah pada geometrinya +Comment[it]=Animazione in dissolvenza quando i suggerimenti e le notifiche cambiano la loro geometria +Comment[ko]=풍선 도움말이나 알림 크기가 변경될 때 크로스페이드 애니메이션 사용 +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[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]=Morphing popups +Name[it]=Finestre a comparsa che si trasformano +Name[ko]=변형되는 팝업 +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[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=http://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..f4bdb5e --- /dev/null +++ b/effects/mouseclick/CMakeLists.txt @@ -0,0 +1,26 @@ +########################## +## configurtion dialog +########################## +set(kwin_mouseclick_config_SRCS mouseclick_config.cpp) +ki18n_wrap_ui(kwin_mouseclick_config_SRCS mouseclick_config.ui) +qt5_add_dbus_interface(kwin_mouseclick_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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::Service + KF5::XmlGui +) + +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..e15c46b --- /dev/null +++ b/effects/mouseclick/mouseclick.cpp @@ -0,0 +1,391 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2012 Filip Wieladek + +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, see . +*********************************************************************/ + +#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, SIGNAL(triggered(bool)), this, SLOT(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, QRegion region, 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 = NULL; + 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 NULL; + } + 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, SIGNAL(mouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers)), + SLOT(slotMouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers))); + effects->startMousePolling(); + } else { + disconnect(effects, SIGNAL(mouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers)), + this, SLOT(slotMouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers))); + 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(), NULL); + 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..afcd170 --- /dev/null +++ b/effects/mouseclick/mouseclick.h @@ -0,0 +1,185 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2012 Filip Wieladek + +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, see . +*********************************************************************/ + +#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(); + virtual void reconfigure(ReconfigureFlags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual void postPaintScreen(); + virtual bool isActive() const; + + // 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..ba516e2 --- /dev/null +++ b/effects/mouseclick/mouseclick_config.cpp @@ -0,0 +1,94 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2012 Filip Wieladek + +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, see . +*********************************************************************/ + +#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, SIGNAL(keyChange()), this, SLOT(changed())); + + // 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..e282007 --- /dev/null +++ b/effects/mouseclick/mouseclick_config.desktop @@ -0,0 +1,55 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_mouseclick_config +X-KDE-ParentComponents=mouseclick + +Name=Mouse Click Animation +Name[bs]=Animacija klika mišem +Name[ca]=Animació de clic de 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]=Mouse Click Animation +Name[is]=Hreyfingar við músarsmell +Name[it]=Animazione del clic del mouse +Name[ja]=マウスクリックアニメーション +Name[kk]=Тышқанды түрту анимациясы +Name[ko]=마우스 클릭 애니메이션 +Name[lt]=Spragtelėjimo pele 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..028662c --- /dev/null +++ b/effects/mouseclick/mouseclick_config.h @@ -0,0 +1,56 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2012 Filip Wieladek + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + virtual ~MouseClickEffectConfig(); + + virtual void save(); + +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..9002f7c --- /dev/null +++ b/effects/mousemark/CMakeLists.txt @@ -0,0 +1,26 @@ +####################################### +# Config +set(kwin_mousemark_config_SRCS mousemark_config.cpp) +ki18n_wrap_ui(kwin_mousemark_config_SRCS mousemark_config.ui) +qt5_add_dbus_interface(kwin_mousemark_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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::Service + KF5::TextWidgets + KF5::XmlGui +) + +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..25ddc0f --- /dev/null +++ b/effects/mousemark/mousemark.cpp @@ -0,0 +1,295 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +#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, SIGNAL(triggered(bool)), this, SLOT(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, SIGNAL(triggered(bool)), this, SLOT(clearLast())); + + connect(effects, SIGNAL(mouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers)), + this, SLOT(slotMouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers))); + connect(effects, SIGNAL(screenLockingChanged(bool)), SLOT(screenLockingChanged(bool))); + 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, QRegion region, 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(), NULL); + 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(), NULL); + 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..2f24f75 --- /dev/null +++ b/effects/mousemark/mousemark.h @@ -0,0 +1,76 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak + +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, see . +*********************************************************************/ + +#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(); + virtual void reconfigure(ReconfigureFlags); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual bool isActive() const; + + // 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..158a3ae --- /dev/null +++ b/effects/mousemark/mousemark_config.cpp @@ -0,0 +1,108 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +#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..22ce828 --- /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[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]=Mouse Mark +Name[is]=Músarspor +Name[it]=Pennarello +Name[ja]=マウスマーク +Name[kk]=Тышқан белгісі +Name[km]=សម្គាល់​កណ្តុរ​ +Name[kn]=ಮೂಷಕ (ಮೌಸ್) ಮುದ್ರೆ +Name[ko]=마우스 자취 +Name[lt]=Piešimas 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[tg]=Рисование отметок на экране +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..290c9ad --- /dev/null +++ b/effects/mousemark/mousemark_config.h @@ -0,0 +1,56 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + virtual ~MouseMarkEffectConfig(); + + virtual void save(); + +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..b49d313 --- /dev/null +++ b/effects/presentwindows/CMakeLists.txt @@ -0,0 +1,33 @@ +####################################### +# 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) +qt5_add_dbus_interface(kwin_presentwindows_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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::Service + KF5::XmlGui +) + +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..0a34197 --- /dev/null +++ b/effects/presentwindows/main.qml @@ -0,0 +1,33 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +import QtQuick 2.0 +import org.kde.plasma.components 2.0 as Plasma + +Item { + width: units.iconSizes.medium + height: width + + Plasma.Button { + id: closeButton + objectName: "closeButton" + iconSource: "window-close" + anchors.fill: parent + } +} diff --git a/effects/presentwindows/presentwindows.cpp b/effects/presentwindows/presentwindows.cpp new file mode 100755 index 0000000..3a6bc6a --- /dev/null +++ b/effects/presentwindows/presentwindows.cpp @@ -0,0 +1,2067 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2008 Lucas Murray + +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, see . +*********************************************************************/ + +#include "presentwindows.h" +//KConfigSkeleton +#include "presentwindowsconfig.h" +#include +#include +#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(NULL) + , m_needInitialSelection(false) + , m_highlightedWindow(NULL) + , m_filterFrame(NULL) + , m_closeView(NULL) + , m_closeWindow(NULL) + , 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, SIGNAL(triggered(bool)), this, SLOT(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, SIGNAL(triggered(bool)), this, SLOT(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, SIGNAL(triggered(bool)), this, SLOT(toggleActiveClass())); + shortcutClass = KGlobalAccel::self()->shortcut(exposeClassAction); + connect(KGlobalAccel::self(), &KGlobalAccel::globalShortcutChanged, this, &PresentWindowsEffect::globalShortcutChanged); + reconfigure(ReconfigureAll); + connect(effects, SIGNAL(windowAdded(KWin::EffectWindow*)), this, SLOT(slotWindowAdded(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowClosed(KWin::EffectWindow*)), this, SLOT(slotWindowClosed(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowDeleted(KWin::EffectWindow*)), this, SLOT(slotWindowDeleted(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowGeometryShapeChanged(KWin::EffectWindow*,QRect)), this, SLOT(slotWindowGeometryShapeChanged(KWin::EffectWindow*,QRect))); + connect(effects, SIGNAL(propertyNotify(KWin::EffectWindow*,long)), this, SLOT(slotPropertyNotify(KWin::EffectWindow*,long))); + connect(effects, &EffectsHandler::numberScreensChanged, this, + [this] { + if (isActive()) + reCreateGrids(); + } + ); +} + +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_closeWindow = 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, QRegion region, ScreenPaintData &data) +{ + effects->paintScreen(mask, region, data); + + // Display the filter box + if (!m_windowFilter.isEmpty()) + m_filterFrame->render(region); +} + +void PresentWindowsEffect::postPaintScreen() +{ + if (m_motionManager.areWindowsMoving()) + effects->addRepaintFull(); + else if (!m_activated && m_motionManager.managingWindows() && !(m_closeWindow && 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(NULL); + 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_closeWindow) { + 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); + if (winData->visible) + w->enablePainting(EffectWindow::PAINT_DISABLED_BY_TAB_GROUP); + + // 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 || w == m_closeWindow || !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(); + if (w == m_closeWindow) { + m_closeWindow = NULL; + } + } 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 { + if (w == m_closeWindow && m_closeView && !m_closeView->isVisible()) { + data.setOpacity(0); + } + 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(); + } + if (m_closeView && w == effects->findWindow(m_closeView->winId())) { + if (m_closeWindow != w) { + DataHash::iterator winDataIt = m_windowData.find(m_closeWindow); + if (winDataIt != m_windowData.end()) { + if (winDataIt->referenced) { + m_closeWindow->unrefWindow(); + } + m_windowData.erase(winDataIt); + } + } + winData->visible = true; + winData->highlight = 1.0; + m_closeWindow = w; + w->setData(WindowForceBlurRole, QVariant(true)); + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + } +} + +void PresentWindowsEffect::slotWindowClosed(EffectWindow *w) +{ + if (m_managerWindow == w) + m_managerWindow = NULL; + 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()); + if (m_closeWindow == w) { + return; // don't rearrange, get's nulled when unref'd + } + 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::slotWindowGeometryShapeChanged(EffectWindow* w, const QRect& old) +{ + Q_UNUSED(old) + if (!m_activated) + return; + if (!m_windowData.contains(w)) + return; + if (w != m_closeWindow) + 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; + } + if (m_closeView) { + const bool contains = m_closeView->geometry().contains(me->pos()); + if (!m_closeView->isVisible() && contains) { + updateCloseWindow(); + } + if (m_closeView->isVisible()) { + const QPoint widgetPos = m_closeView->mapFromGlobal(me->pos()); +// const QPointF scenePos = m_closeView->mapToScene(widgetPos); + QMouseEvent event(me->type(), widgetPos, me->pos(), me->button(), me->buttons(), me->modifiers()); + m_closeView->windowInputMouseEvent(&event); + if (contains) { + // filter out + 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 = NULL; + 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(NULL); + 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::MidButton) { + 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(quint32 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(quint32 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(quint32 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; + 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(NULL); + 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 = NULL; // 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]; + 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; + } + } + } + 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()); + qSort(windowlist); // 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. + qSort(windowlist); + + 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() - (area.width() - 20 - bounds.width() * scale) / 2 - 10 / scale, + bounds.y() - (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 successfull 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 = NULL; + m_windowFilter.clear(); + + if (!(m_doNotCloseWindows || m_closeView)) { + m_closeView = new CloseWindowView(); + 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() && (w->isCurrentTab() || winData->visible); + } + 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 = NULL; + } + } + 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->isCurrentTab()) + return false; + if (w->isSkipSwitcher()) + return false; + if (m_closeView && w == effects->findWindow(m_closeView->winId())) + 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() || w == m_closeWindow) + return true; + return isSelectableWindow(w); +} + +void PresentWindowsEffect::setHighlightedWindow(EffectWindow *w) +{ + if (w == m_highlightedWindow || (w != NULL && !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::elevateCloseWindow() +{ + if (!m_closeView || !m_activated) + return; + if (EffectWindow *cw = effects->findWindow(m_closeView->winId())) + effects->setElevatedWindow(cw, true); +} + +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->width() > rect.width() && 2*m_closeView->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(); + // to wait for the next event cycle (or more if the show takes more time) + // TODO: make the closeWindow a graphicsviewitem? why should there be an extra scene to be used in an exiting scene?? + QTimer::singleShot(50, this, SLOT(elevateCloseWindow())); + } + 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 = NULL; + 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 == NULL) + next = e; + else { + QRectF nArea = m_motionManager.transformedGeometry(next); + if (eArea.x() < nArea.x()) + next = e; + } + } + } + if (next == NULL) { + 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 = NULL; + 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 == NULL) + next = e; + else { + QRectF nArea = m_motionManager.transformedGeometry(next); + if (eArea.x() + eArea.width() > nArea.x() + nArea.width()) + next = e; + } + } + } + if (next == NULL) { + 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 = NULL; + 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 == NULL) + next = e; + else { + QRectF nArea = m_motionManager.transformedGeometry(next); + if (eArea.y() < nArea.y()) + next = e; + } + } + } + if (next == NULL) { + 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 = NULL; + 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 == NULL) + next = e; + else { + QRectF nArea = m_motionManager.transformedGeometry(next); + if (eArea.y() + eArea.height() > nArea.y() + nArea.height()) + next = e; + } + } + } + if (next == NULL) { + 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 = NULL; + 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 == NULL) { + 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::CloseWindowView(QObject *parent) + : QObject(parent) + , m_armTimer(new QElapsedTimer()) + , m_window(new QQuickView()) + , m_visible(false) + , m_posIsValid(false) +{ + m_window->setFlags(Qt::X11BypassWindowManagerHint | Qt::FramelessWindowHint); + m_window->setColor(Qt::transparent); + + m_window->setSource(QUrl(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kwin/effects/presentwindows/main.qml")))); + if (QObject *item = m_window->rootObject()->findChild(QStringLiteral("closeButton"))) { + connect(item, SIGNAL(clicked()), SIGNAL(requestClose())); + } + + m_armTimer->restart(); +} + +void CloseWindowView::windowInputMouseEvent(QMouseEvent *e) +{ + if (e->type() == QEvent::MouseMove) { + qApp->sendEvent(m_window.data(), e); + } else if (!m_armTimer->hasExpired(350)) { + // 50ms until the window is elevated (seen!) and 300ms more to be "realized" by the user. + return; + } + qApp->sendEvent(m_window.data(), e); +} + +void CloseWindowView::disarm() +{ + m_armTimer->restart(); +} + +bool CloseWindowView::isVisible() const +{ + return m_visible; +} + +void CloseWindowView::show() +{ + if (!m_visible && m_posIsValid) { + m_window->setPosition(m_pos); + m_posIsValid = false; + } + m_visible = true; + m_window->show(); +} + +void CloseWindowView::hide() +{ + if (!m_posIsValid) { + m_pos = m_window->position(); + m_posIsValid = true; + m_window->setPosition(-m_window->width(), -m_window->height()); + } + m_visible = false; + QEvent event(QEvent::Leave); + qApp->sendEvent(m_window.data(), &event); +} + +#define DELEGATE(type, name) \ +type CloseWindowView::name() const \ +{ \ + return m_window->name(); \ +} + +DELEGATE(int, width) +DELEGATE(int, height) +DELEGATE(QSize, size) +DELEGATE(QRect, geometry) +DELEGATE(WId, winId) + +#undef DELEGATE + +void CloseWindowView::setGeometry(const QRect &geometry) +{ + m_posIsValid = false; + m_window->setGeometry(geometry); +} + +QPoint CloseWindowView::mapFromGlobal(const QPoint &pos) const +{ + return m_window->mapFromGlobal(pos); +} + +} // namespace + diff --git a/effects/presentwindows/presentwindows.h b/effects/presentwindows/presentwindows.h new file mode 100644 index 0000000..6e95bda --- /dev/null +++ b/effects/presentwindows/presentwindows.h @@ -0,0 +1,351 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2008 Lucas Murray + +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, see . +*********************************************************************/ + +#ifndef KWIN_PRESENTWINDOWS_H +#define KWIN_PRESENTWINDOWS_H + +#include "presentwindows_proxy.h" + +#include + +class QMouseEvent; +class QElapsedTimer; +class QQuickView; + +namespace KWin +{ +class CloseWindowView : public QObject +{ + Q_OBJECT +public: + explicit CloseWindowView(QObject *parent = 0); + void windowInputMouseEvent(QMouseEvent* e); + void disarm(); + + void show(); + void hide(); + bool isVisible() const; + + // delegate to QWindow + int width() const; + int height() const; + QSize size() const; + QRect geometry() const; + WId winId() const; + void setGeometry(const QRect &geometry); + QPoint mapFromGlobal(const QPoint &pos) const; + +Q_SIGNALS: + void requestClose(); + +private: + QScopedPointer m_armTimer; + QScopedPointer m_window; + bool m_visible; + QPoint m_pos; + bool m_posIsValid; +}; + +/** + * 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(); + virtual ~PresentWindowsEffect(); + + virtual void reconfigure(ReconfigureFlags); + virtual void* proxy(); + + // Screen painting + virtual void prePaintScreen(ScreenPrePaintData &data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData &data); + virtual void postPaintScreen(); + + // Window painting + virtual void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time); + virtual void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data); + + // User interaction + virtual bool borderActivated(ElectricBorder border); + virtual void windowInputMouseEvent(QEvent *e); + virtual void grabbedKeyboardEvent(QKeyEvent *e); + virtual bool isActive() const; + + bool touchDown(quint32 id, const QPointF &pos, quint32 time) override; + bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override; + bool touchUp(quint32 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 // Minimize 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 slotWindowGeometryShapeChanged(KWin::EffectWindow *w, const QRect &old); + // atoms + void slotPropertyNotify(KWin::EffectWindow* w, long atom); + +private Q_SLOTS: + void closeWindow(); + void elevateCloseWindow(); + +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; + EffectWindow* m_closeWindow; + Qt::Corner m_closeButtonCorner; + struct { + quint32 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..fe38376 --- /dev/null +++ b/effects/presentwindows/presentwindows_config.cpp @@ -0,0 +1,118 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2008 Lucas Murray + +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, see . +*********************************************************************/ + +#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, SIGNAL(keyChange()), this, SLOT(changed())); + + 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..2f93930 --- /dev/null +++ b/effects/presentwindows/presentwindows_config.desktop @@ -0,0 +1,85 @@ +[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[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]=Present Windows +Name[is]=Núverandi gluggar +Name[it]=Presenta le finestre +Name[ja]=ウィンドウを並べて表示 +Name[kk]=Терезелерді көрсету +Name[km]=បង្ហាញ​បង្អួច​ +Name[kn]=ಪ್ರಸಕ್ತ ಕಿಟಕಿಗಳು +Name[ko]=창 진열하기 +Name[lt]=Dabar atverti langai +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]=Ferestre prezente +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[tg]=Ресурсы Windows +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..6eae72b --- /dev/null +++ b/effects/presentwindows/presentwindows_config.h @@ -0,0 +1,57 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2008 Lucas Murray + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + ~PresentWindowsEffectConfig(); + +public Q_SLOTS: + virtual void save(); + virtual void defaults(); + +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..54bb80e --- /dev/null +++ b/effects/presentwindows/presentwindows_config.ui @@ -0,0 +1,482 @@ + + + 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 + + + + + + + + Right button: + + + kcfg_RightButtonWindow + + + + + + + + No action + + + + + Activate window + + + + + End effect + + + + + Bring window to current desktop + + + + + Send window to all desktops + + + + + (Un-)Minimize 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..f6aca87 --- /dev/null +++ b/effects/presentwindows/presentwindows_proxy.cpp @@ -0,0 +1,47 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +#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..8c8bd29 --- /dev/null +++ b/effects/presentwindows/presentwindows_proxy.h @@ -0,0 +1,46 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +#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..763f2da --- /dev/null +++ b/effects/resize/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_resize_config_SRCS resize_config.cpp) +ki18n_wrap_ui(kwin_resize_config_SRCS resize_config.ui) +qt5_add_dbus_interface(kwin_resize_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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 + Qt5::DBus + KF5::ConfigWidgets + KF5::I18n + KF5::Service +) + +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..e103562 --- /dev/null +++ b/effects/resize/resize.cpp @@ -0,0 +1,177 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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(0) +{ + initConfig(); + reconfigure(ReconfigureAll); + connect(effects, SIGNAL(windowStartUserMovedResized(KWin::EffectWindow*)), this, SLOT(slotWindowStartUserMovedResized(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowStepUserMovedResized(KWin::EffectWindow*,QRect)), this, SLOT(slotWindowStepUserMovedResized(KWin::EffectWindow*,QRect))); + connect(effects, SIGNAL(windowFinishUserMovedResized(KWin::EffectWindow*)), this, SLOT(slotWindowFinishUserMovedResized(KWin::EffectWindow*))); +} + +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.rects().count() * 12); + foreach (const QRect & r, paintRegion.rects()) { + 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(), NULL); + vbo->render(GL_TRIANGLES); + glDisable(GL_BLEND); + } + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() == XRenderCompositing) { + QVector rects; + foreach (const QRect & r, paintRegion.rects()) { + 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); + foreach (const QRect &r, paintRegion.rects()) { + 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 = NULL; + 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..4b79bbc --- /dev/null +++ b/effects/resize/resize.h @@ -0,0 +1,73 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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(); + virtual inline bool provides(Effect::Feature ef) { + return ef == Effect::Resize; + } + inline bool isActive() const { return m_active || AnimationEffect::isActive(); } + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time); + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual void reconfigure(ReconfigureFlags); + + 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..647a5df --- /dev/null +++ b/effects/resize/resize_config.cpp @@ -0,0 +1,70 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2010 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..23eb846 --- /dev/null +++ b/effects/resize/resize_config.desktop @@ -0,0 +1,75 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_resize_config +X-KDE-ParentComponents=resize + +Name=Resize Window +Name[ar]=غير حجم النافذة +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]=Resize 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[tg]=Тағйири андозаи тиреза +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..f9742e0 --- /dev/null +++ b/effects/resize/resize_config.h @@ -0,0 +1,54 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2010 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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 = 0); +}; + +class ResizeEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit ResizeEffectConfig(QWidget* parent = 0, const QVariantList& args = QVariantList()); + +public Q_SLOTS: + virtual void save(); + +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/CMakeLists.txt b/effects/scale/CMakeLists.txt new file mode 100644 index 0000000..016b2f6 --- /dev/null +++ b/effects/scale/CMakeLists.txt @@ -0,0 +1,25 @@ +####################################### +# Config +set(kwin_scale_config_SRCS scale_config.cpp) +ki18n_wrap_ui(kwin_scale_config_SRCS scale_config.ui) +qt5_add_dbus_interface(kwin_scale_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +kconfig_add_kcfg_files(kwin_scale_config_SRCS scaleconfig.kcfgc) + +add_library(kwin_scale_config MODULE ${kwin_scale_config_SRCS}) + +target_link_libraries(kwin_scale_config + Qt5::DBus + KF5::ConfigWidgets + KF5::I18n + KF5::Service +) + +kcoreaddons_desktop_to_json(kwin_scale_config scale_config.desktop SERVICE_TYPES kcmodule.desktop) + +install( + TARGETS + kwin_scale_config + DESTINATION + ${PLUGIN_INSTALL_DIR}/kwin/effects/configs +) + diff --git a/effects/scale/scale.cpp b/effects/scale/scale.cpp new file mode 100644 index 0000000..f3d41c0 --- /dev/null +++ b/effects/scale/scale.cpp @@ -0,0 +1,285 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +// own +#include "scale.h" + +// KConfigSkeleton +#include "scaleconfig.h" + +// Qt +#include + +namespace KWin +{ + +static const QSet s_blacklist { + // The logout screen has to be animated only by the logout effect. + QStringLiteral("ksmserver ksmserver"), + QStringLiteral("ksmserver-logout-greeter ksmserver-logout-greeter"), + + // KDE Plasma splash screen has to be animated only by the login effect. + QStringLiteral("ksplashqml ksplashqml"), + QStringLiteral("ksplashsimple ksplashsimple"), + QStringLiteral("ksplashx ksplashx") +}; + +ScaleEffect::ScaleEffect() +{ + initConfig(); + reconfigure(ReconfigureAll); + + connect(effects, &EffectsHandler::windowAdded, this, &ScaleEffect::windowAdded); + connect(effects, &EffectsHandler::windowClosed, this, &ScaleEffect::windowClosed); + connect(effects, &EffectsHandler::windowDeleted, this, &ScaleEffect::windowDeleted); + connect(effects, &EffectsHandler::windowDataChanged, this, &ScaleEffect::windowDataChanged); +} + +ScaleEffect::~ScaleEffect() +{ +} + +void ScaleEffect::reconfigure(ReconfigureFlags flags) +{ + Q_UNUSED(flags) + + ScaleConfig::self()->read(); + m_duration = std::chrono::milliseconds(animationTime(160)); + + m_inParams.scale.from = ScaleConfig::inScale(); + m_inParams.scale.to = 1.0; + m_inParams.opacity.from = ScaleConfig::inOpacity(); + m_inParams.opacity.to = 1.0; + + m_outParams.scale.from = 1.0; + m_outParams.scale.to = ScaleConfig::outScale(); + m_outParams.opacity.from = 1.0; + m_outParams.opacity.to = ScaleConfig::outOpacity(); +} + +void ScaleEffect::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; + } + + effects->prePaintScreen(data, time); +} + +void ScaleEffect::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 ScaleEffect::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 ScaleParams params = w->isDeleted() ? m_outParams : m_inParams; + const qreal t = (*animationIt).value(); + const qreal scale = interpolate(params.scale.from, params.scale.to, t); + + data.setXScale(scale); + data.setYScale(scale); + data.setXTranslation(0.5 * (1.0 - scale) * w->width()); + data.setYTranslation(0.5 * (1.0 - scale) * w->height()); + data.multiplyOpacity(interpolate(params.opacity.from, params.opacity.to, t)); + + effects->paintWindow(w, mask, region, data); +} + +void ScaleEffect::postPaintScreen() +{ + auto animationIt = m_animations.begin(); + while (animationIt != m_animations.end()) { + EffectWindow *w = animationIt.key(); + + const QRect geo = w->expandedGeometry(); + const ScaleParams params = w->isDeleted() ? m_outParams : m_inParams; + const qreal scale = qMax(params.scale.from, params.scale.to); + const QRect repaintRect( + geo.topLeft() + 0.5 * (1.0 - scale) * QPoint(geo.width(), geo.height()), + geo.size() * scale); + effects->addRepaint(repaintRect); + + if ((*animationIt).done()) { + if (w->isDeleted()) { + w->unrefWindow(); + } else { + w->setData(WindowForceBackgroundContrastRole, QVariant()); + w->setData(WindowForceBlurRole, QVariant()); + } + animationIt = m_animations.erase(animationIt); + } else { + ++animationIt; + } + } + + effects->postPaintScreen(); +} + +bool ScaleEffect::isActive() const +{ + return !m_animations.isEmpty(); +} + +bool ScaleEffect::supported() +{ + return effects->animationsSupported(); +} + +void ScaleEffect::windowAdded(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!isScaleWindow(w)) { + return; + } + + if (!w->isVisible()) { + return; + } + + const void *addGrab = w->data(WindowAddedGrabRole).value(); + if (addGrab && addGrab != this) { + return; + } + + TimeLine &timeLine = m_animations[w]; + timeLine.reset(); + timeLine.setDirection(TimeLine::Forward); + timeLine.setDuration(m_duration); + timeLine.setEasingCurve(QEasingCurve::InCurve); + + w->setData(WindowAddedGrabRole, QVariant::fromValue(static_cast(this))); + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + w->setData(WindowForceBlurRole, QVariant(true)); + + w->addRepaintFull(); +} + +void ScaleEffect::windowClosed(EffectWindow *w) +{ + if (effects->activeFullScreenEffect()) { + return; + } + + if (!isScaleWindow(w)) { + return; + } + + if (!w->isVisible()) { + return; + } + + const void *closeGrab = w->data(WindowClosedGrabRole).value(); + if (closeGrab && closeGrab != this) { + return; + } + + w->refWindow(); + + TimeLine &timeLine = m_animations[w]; + timeLine.reset(); + timeLine.setDirection(TimeLine::Forward); + timeLine.setDuration(m_duration); + timeLine.setEasingCurve(QEasingCurve::OutCurve); + + w->setData(WindowClosedGrabRole, QVariant::fromValue(static_cast(this))); + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + w->setData(WindowForceBlurRole, QVariant(true)); + + w->addRepaintFull(); +} + +void ScaleEffect::windowDeleted(EffectWindow *w) +{ + m_animations.remove(w); +} + +void ScaleEffect::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); + + w->setData(WindowForceBackgroundContrastRole, QVariant()); + w->setData(WindowForceBlurRole, QVariant()); +} + +bool ScaleEffect::isScaleWindow(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")) { + return w->hasDecoration(); + } + + if (s_blacklist.contains(w->windowClass())) { + return false; + } + + if (w->hasDecoration()) { + return true; + } + + if (!w->isManaged()) { + return false; + } + + return w->isNormalWindow() + || w->isDialog(); +} + +} // namespace KWin diff --git a/effects/scale/scale.h b/effects/scale/scale.h new file mode 100644 index 0000000..68f3cff --- /dev/null +++ b/effects/scale/scale.h @@ -0,0 +1,116 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#ifndef KWIN_SCALE_H +#define KWIN_SCALE_H + +// kwineffects +#include + +namespace KWin +{ + +class ScaleEffect : public Effect +{ + Q_OBJECT + Q_PROPERTY(int duration READ duration) + Q_PROPERTY(qreal inScale READ inScale) + Q_PROPERTY(qreal inOpacity READ inOpacity) + Q_PROPERTY(qreal outScale READ outScale) + Q_PROPERTY(qreal outOpacity READ outOpacity) + +public: + ScaleEffect(); + ~ScaleEffect() 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(); + + int duration() const; + qreal inScale() const; + qreal inOpacity() const; + qreal outScale() 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 isScaleWindow(EffectWindow *w) const; + + std::chrono::milliseconds m_duration; + QHash m_animations; + + struct ScaleParams { + struct { + qreal from; + qreal to; + } scale, opacity; + }; + + ScaleParams m_inParams; + ScaleParams m_outParams; +}; + +inline int ScaleEffect::requestedEffectChainPosition() const +{ + return 50; +} + +inline int ScaleEffect::duration() const +{ + return m_duration.count(); +} + +inline qreal ScaleEffect::inScale() const +{ + return m_inParams.scale.from; +} + +inline qreal ScaleEffect::inOpacity() const +{ + return m_inParams.opacity.from; +} + +inline qreal ScaleEffect::outScale() const +{ + return m_outParams.scale.to; +} + +inline qreal ScaleEffect::outOpacity() const +{ + return m_outParams.opacity.to; +} + +} // namespace KWin + +#endif diff --git a/effects/scale/scale.kcfg b/effects/scale/scale.kcfg new file mode 100644 index 0000000..2b77c30 --- /dev/null +++ b/effects/scale/scale.kcfg @@ -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/scale_config.cpp b/effects/scale/scale_config.cpp new file mode 100644 index 0000000..8b078ad --- /dev/null +++ b/effects/scale/scale_config.cpp @@ -0,0 +1,62 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#include "scale_config.h" + +// KConfigSkeleton +#include "scaleconfig.h" +#include + +#include +#include +#include + +K_PLUGIN_FACTORY_WITH_JSON(ScaleEffectConfigFactory, + "scale_config.json", + registerPlugin();) + +namespace KWin +{ + +ScaleEffectConfig::ScaleEffectConfig(QWidget *parent, const QVariantList &args) + : KCModule(KAboutData::pluginData(QStringLiteral("scale")), parent, args) +{ + ui.setupUi(this); + ScaleConfig::instance(KWIN_CONFIG); + addConfig(ScaleConfig::self(), this); + load(); +} + +ScaleEffectConfig::~ScaleEffectConfig() +{ +} + +void ScaleEffectConfig::save() +{ + KCModule::save(); + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + interface.reconfigureEffect(QStringLiteral("scale")); +} + +} // namespace KWin + +#include "scale_config.moc" diff --git a/effects/scale/scale_config.desktop b/effects/scale/scale_config.desktop new file mode 100644 index 0000000..0c4b2ce --- /dev/null +++ b/effects/scale/scale_config.desktop @@ -0,0 +1,36 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_scale_config +X-KDE-ParentComponents=scale + +Name=Scale +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[eu]=Eskalatu +Name[fi]=Skaalaa +Name[fr]=Échelle +Name[gl]=Cambiar as dimensións +Name[hu]=Nagyítás +Name[id]=Scale +Name[it]=Scala +Name[ko]=크기 조정 +Name[nl]=Schalen +Name[nn]=Skalering +Name[pl]=Skala +Name[pt]=Escala +Name[pt_BR]=Escala +Name[ru]=Масштабирование +Name[sk]=Škálovať +Name[sv]=Skala +Name[uk]=Масштабування +Name[x-test]=xxScalexx +Name[zh_CN]=比例 +Name[zh_TW]=縮放 diff --git a/effects/scale/scale_config.h b/effects/scale/scale_config.h new file mode 100644 index 0000000..d26f361 --- /dev/null +++ b/effects/scale/scale_config.h @@ -0,0 +1,48 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#ifndef SCALE_CONFIG_H +#define SCALE_CONFIG_H + +#include "ui_scale_config.h" + +#include + +namespace KWin +{ + +class ScaleEffectConfig : public KCModule +{ + Q_OBJECT + +public: + explicit ScaleEffectConfig(QWidget *parent = nullptr, const QVariantList &args = QVariantList()); + ~ScaleEffectConfig() override; + + void save() override; + +private: + ::Ui::ScaleEffectConfig ui; +}; + +} // namespace KWin + +#endif + diff --git a/effects/scale/scale_config.ui b/effects/scale/scale_config.ui new file mode 100644 index 0000000..97b43b5 --- /dev/null +++ b/effects/scale/scale_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/scaleconfig.kcfgc b/effects/scale/scaleconfig.kcfgc new file mode 100644 index 0000000..2b9bc99 --- /dev/null +++ b/effects/scale/scaleconfig.kcfgc @@ -0,0 +1,5 @@ +File=scale.kcfg +ClassName=ScaleConfig +NameSpace=KWin +Singleton=true +Mutators=true diff --git a/effects/screenedge/CMakeLists.txt b/effects/screenedge/CMakeLists.txt new file mode 100644 index 0000000..fb1240e --- /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..1b8dea0 --- /dev/null +++ b/effects/screenedge/screenedgeeffect.cpp @@ -0,0 +1,361 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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, SIGNAL(screenEdgeApproaching(ElectricBorder,qreal,QRect)), SLOT(edgeApproaching(ElectricBorder,qreal,QRect))); + m_cleanupTimer->setInterval(5000); + m_cleanupTimer->setSingleShot(true); + connect(m_cleanupTimer, SIGNAL(timeout()), SLOT(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, QRegion region, 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_SRC_ALPHA, 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 NULL; + } + } 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 NULL; + } +#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 NULL; + } + } + + 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 NULL; + } +} + +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(); + + 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 NULL; + } + QPixmap image(size); + image.fill(Qt::transparent); + QPainter p; + p.begin(&image); + if (border == ElectricBottom || border == ElectricTop) { + p.drawPixmap(pixmapPosition, l); + p.drawTiledPixmap(QRect(l.width(), pixmapPosition.y(), size.width() - l.width() - r.width(), c.height()), c); + p.drawPixmap(QPoint(size.width() - r.width(), pixmapPosition.y()), r); + } else { + p.drawPixmap(pixmapPosition, l); + p.drawTiledPixmap(QRect(pixmapPosition.x(), l.height(), c.width(), size.height() - l.height() - r.height()), 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..d940e61 --- /dev/null +++ b/effects/screenedge/screenedgeeffect.h @@ -0,0 +1,79 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(); + virtual ~ScreenEdgeEffect(); + virtual void prePaintScreen(ScreenPrePaintData &data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData &data); + virtual bool isActive() const; + + 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..ddf46df --- /dev/null +++ b/effects/screenshot/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# Effect + +# Source files +set( kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + screenshot/screenshot.cpp + ) diff --git a/effects/screenshot/screenshot.cpp b/effects/screenshot/screenshot.cpp new file mode 100644 index 0000000..5b7c7cd --- /dev/null +++ b/effects/screenshot/screenshot.cpp @@ -0,0 +1,655 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2010 Martin Gräßlin + Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies) + +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, see . +*********************************************************************/ +#include "screenshot.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +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_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"); + +bool ScreenShotEffect::supported() +{ + return effects->compositingType() == XRenderCompositing || + (effects->isOpenGLCompositing() && GLRenderTarget::supported()); +} + +ScreenShotEffect::ScreenShotEffect() + : m_scheduledScreenshot(0) +{ + connect ( effects, SIGNAL(windowClosed(KWin::EffectWindow*)), SLOT(windowClosed(KWin::EffectWindow*)) ); + 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 + +void ScreenShotEffect::paintScreen(int mask, QRegion region, ScreenPaintData &data) +{ + m_cachedOutputGeometry = data.outputGeometry(); + effects->paintScreen(mask, region, data); +} + +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) { + foreach (const WindowQuad & quad, 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; + foreach (const WindowQuad & quad, 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.byteCount(), (GLvoid*)img.bits()); + GLRenderTarget::popRenderTarget(); + ScreenShotEffect::convertFromGLImage(img, width, height); + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + xcb_image_t *xImage = NULL; + 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 int depth = img.depth(); + xcb_pixmap_t xpix = xcb_generate_id(xcbConnection()); + xcb_create_pixmap(xcbConnection(), depth, xpix, x11RootWindow(), img.width(), img.height()); + + xcb_gcontext_t cid = xcb_generate_id(xcbConnection()); + xcb_create_gc(xcbConnection(), cid, xpix, 0, NULL); + xcb_put_image(xcbConnection(), XCB_IMAGE_FORMAT_Z_PIXMAP, xpix, cid, img.width(), img.height(), + 0, 0, 0, depth, img.byteCount(), img.constBits()); + xcb_free_gc(xcbConnection(), cid); + xcb_flush(xcbConnection()); + emit screenshotCreated(xpix); + m_windowMode = WindowMode::NoCapture; + } else if (m_windowMode == WindowMode::File) { + sendReplyImage(img); + } else if (m_windowMode == WindowMode::FileDescriptor) { + 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_windowMode = WindowMode::NoCapture; + m_fd = -1; + } +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (xImage) { + xcb_image_destroy(xImage); + } +#endif + } + m_scheduledScreenshot = NULL; + } + + 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; + } + const QImage img = blitScreenshot(intersection); + if (img.size() == m_scheduledGeometry.size()) { + // we are done + sendReplyImage(img); + return; + } + if (m_multipleOutputsImage.isNull()) { + m_multipleOutputsImage = QImage(m_scheduledGeometry.size(), QImage::Format_ARGB32); + m_multipleOutputsImage.fill(Qt::transparent); + } + QPainter p; + p.begin(&m_multipleOutputsImage); + p.drawImage(intersection.topLeft() - m_scheduledGeometry.topLeft(), img); + p.end(); + m_multipleOutputsRendered = m_multipleOutputsRendered.united(intersection); + if (m_multipleOutputsRendered.boundingRect() == m_scheduledGeometry) { + sendReplyImage(m_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_multipleOutputsImage = QImage(); + m_multipleOutputsRendered = QRegion(); + m_captureCursor = false; + m_windowMode = WindowMode::NoCapture; +} + +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 = 0; + } + if (m_scheduledScreenshot) { + 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(); + } +} + +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 (!calledFromDBus()) { + return QString(); + } + if (isTakingScreenshot()) { + sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); + 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) +{ + 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_captureCursor = captureCursor; + + 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->virtualScreenGeometry(); + effects->addRepaint(m_scheduledGeometry); + } + } + ); +} + +QString ScreenShotEffect::screenshotScreen(int screen, bool captureCursor) +{ + if (!calledFromDBus()) { + return QString(); + } + if (isTakingScreenshot()) { + sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); + return QString(); + } + m_scheduledGeometry = effects->clientArea(FullScreenArea, screen, 0); + if (m_scheduledGeometry.isNull()) { + sendErrorReply(s_errorInvalidScreen, s_errorInvalidScreenMsg); + return QString(); + } + m_captureCursor = captureCursor; + m_replyMessage = message(); + setDelayedReply(true); + effects->addRepaint(m_scheduledGeometry); + return QString(); +} + +void ScreenShotEffect::screenshotScreen(QDBusUnixFileDescriptor fd, bool captureCursor) +{ + 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_captureCursor = captureCursor; + + 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 (!calledFromDBus()) { + return QString(); + } + if (isTakingScreenshot()) { + sendErrorReply(s_errorAlreadyTaking, s_errorAlreadyTakingMsg); + 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) +{ + QImage img; + if (effects->isOpenGLCompositing()) + { + if (!GLRenderTarget::blitSupported()) { + qCDebug(KWINEFFECTS) << "Framebuffer Blit not supported"; + return img; + } + GLTexture tex(GL_RGBA8, geometry.width(), geometry.height()); + GLRenderTarget target(tex); + target.blitFromFramebuffer(geometry); + // copy content from framebuffer into image + tex.bind(); + img = QImage(geometry.size(), QImage::Format_ARGB32); + if (GLPlatform::instance()->isGLES()) { + glReadPixels(0, 0, img.width(), img.height(), GL_RGBA, GL_UNSIGNED_BYTE, (GLvoid*)img.bits()); + } else { + glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, (GLvoid*)img.bits()); + } + tex.unbind(); + ScreenShotEffect::convertFromGLImage(img, geometry.width(), geometry.height()); + } + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + if (effects->compositingType() == XRenderCompositing) { + xcb_image_t *xImage = NULL; + img = xPictureToImage(effects->xrenderBufferPicture(), geometry, &xImage); + if (xImage) { + xcb_image_destroy(xImage); + } + } +#endif + + if (m_captureCursor) { + grabPointerImage(img, geometry.x(), geometry.y()); + } + + 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 + // Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies) + // see http://qt.gitorious.org/qt/qt/blobs/master/src/opengl/qgl.cpp + if (QSysInfo::ByteOrder == QSysInfo::BigEndian) { + // OpenGL gives RGBA; Qt wants ARGB + uint *p = (uint*)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 = (uint*)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 != NULL || !m_scheduledGeometry.isNull()) && !effects->isScreenLocked(); +} + +void ScreenShotEffect::windowClosed( EffectWindow* w ) +{ + if (w == m_scheduledScreenshot) { + m_scheduledScreenshot = NULL; + 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..7a9458a --- /dev/null +++ b/effects/screenshot/screenshot.h @@ -0,0 +1,173 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2010 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef KWIN_SCREENSHOT_H +#define KWIN_SCREENSHOT_H + +#include +#include +#include +#include +#include +#include +#include + +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(); + virtual ~ScreenShotEffect(); + void paintScreen(int mask, QRegion region, ScreenPaintData &data) override; + virtual void postPaintScreen(); + virtual bool isActive() const; + + 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 + **/ + Q_SCRIPTABLE void screenshotFullscreen(QDBusUnixFileDescriptor fd, bool captureCursor = 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); + QString saveTempImage(const QImage &img); + void sendReplyImage(const QImage &img); + enum class InfoMessageMode { + Window, + Screen + }; + void showInfoMessage(InfoMessageMode mode); + void hideInfoMessage(); + bool isTakingScreenshot() const; + EffectWindow *m_scheduledScreenshot; + ScreenShotType m_type; + QRect m_scheduledGeometry; + QDBusMessage m_replyMessage; + QRect m_cachedOutputGeometry; + QImage m_multipleOutputsImage; + QRegion m_multipleOutputsRendered; + bool m_captureCursor = false; + enum class WindowMode { + NoCapture, + Xpixmap, + File, + FileDescriptor + }; + WindowMode m_windowMode = WindowMode::NoCapture; + int m_fd = -1; +}; + +} // namespace + +#endif // KWIN_SCREENSHOT_H diff --git a/effects/shaders.qrc b/effects/shaders.qrc new file mode 100644 index 0000000..ddb715d --- /dev/null +++ b/effects/shaders.qrc @@ -0,0 +1,22 @@ + + + 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/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 + + + diff --git a/effects/sheet/CMakeLists.txt b/effects/sheet/CMakeLists.txt new file mode 100644 index 0000000..ffd0a8b --- /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..eea7198 --- /dev/null +++ b/effects/sheet/sheet.cpp @@ -0,0 +1,228 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Philip Falkner +Copyright (C) 2009 Martin Gräßlin +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +// 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() + : 500); + 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)); + + 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..91afef7 --- /dev/null +++ b/effects/sheet/sheet.h @@ -0,0 +1,85 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Philip Falkner +Copyright (C) 2009 Martin Gräßlin +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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..564c4e7 --- /dev/null +++ b/effects/showfps/CMakeLists.txt @@ -0,0 +1,25 @@ +####################################### +# Config +set(kwin_showfps_config_SRCS showfps_config.cpp) +ki18n_wrap_ui(kwin_showfps_config_SRCS showfps_config.ui) +qt5_add_dbus_interface(kwin_showfps_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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 + Qt5::DBus + KF5::Completion + KF5::ConfigWidgets + KF5::I18n + KF5::Service +) + +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..b17aa23 --- /dev/null +++ b/effects/showfps/showfps.cpp @@ -0,0 +1,547 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak + +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, see . +*********************************************************************/ + +#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) +{ + if (time == 0) { + // TODO optimized away + } + t.start(); + frames[ frames_pos ] = t.minute() * 60000 + t.second() * 1000 + t.msec(); + if (++frames_pos == MAX_FPS) + frames_pos = 0; + effects->prePaintScreen(data, time); + data.paint += fps_rect; + + paint_size[ paints_pos ] = 0; +} + +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; + foreach (const QRect & r, r2.rects()) + winsize += r.width() * r.height(); + paint_size[ paints_pos ] += winsize; +} + +void ShowFpsEffect::paintScreen(int mask, QRegion region, ScreenPaintData& data) +{ + effects->paintScreen(mask, region, data); + int fps = 0; + for (int i = 0; + i < MAX_FPS; + ++i) + if (abs(t.minute() * 60000 + t.second() * 1000 + t.msec() - 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(), NULL); + 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(), NULL); + 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(), NULL); + 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(), NULL); + 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(), NULL); + 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(), NULL); + 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..40c6a86 --- /dev/null +++ b/effects/showfps/showfps.h @@ -0,0 +1,109 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak + +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, see . +*********************************************************************/ + +#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(); + virtual void reconfigure(ReconfigureFlags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual void postPaintScreen(); + 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); + QTime 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 }; + int frames[ MAX_FPS ]; // (sec*1000+msec) of the time 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..ae89083 --- /dev/null +++ b/effects/showfps/showfps_config.cpp @@ -0,0 +1,67 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks + +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, see . +*********************************************************************/ + +#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..7ac4d2b --- /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[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]=Show 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]=FPS rodymas +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]=Afișează CPS +Name[ru]=График производительности +Name[se]=Čájet rámmaid sekunddas +Name[si]=FPS පෙන්වන්න +Name[sk]=Zobraziť FPS +Name[sl]=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[tg]=Намоиши 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]=顯示每秒幾張 diff --git a/effects/showfps/showfps_config.h b/effects/showfps/showfps_config.h new file mode 100644 index 0000000..9858d5f --- /dev/null +++ b/effects/showfps/showfps_config.h @@ -0,0 +1,47 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + ~ShowFpsEffectConfig(); + +public Q_SLOTS: + virtual void save(); + +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..64a226b --- /dev/null +++ b/effects/showpaint/CMakeLists.txt @@ -0,0 +1,7 @@ +####################################### +# Effect + +# Source files +set( kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + showpaint/showpaint.cpp + ) diff --git a/effects/showpaint/showpaint.cpp b/effects/showpaint/showpaint.cpp new file mode 100644 index 0000000..229cf05 --- /dev/null +++ b/effects/showpaint/showpaint.cpp @@ -0,0 +1,126 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak +Copyright (C) 2010 Martin Gräßlin + +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, see . +*********************************************************************/ + +#include "showpaint.h" + +#include +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#endif + +#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 +}; + +void ShowPaintEffect::paintScreen(int mask, QRegion region, 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); + } +} + +} // namespace KWin diff --git a/effects/showpaint/showpaint.h b/effects/showpaint/showpaint.h new file mode 100644 index 0000000..abd1e19 --- /dev/null +++ b/effects/showpaint/showpaint.h @@ -0,0 +1,48 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak + +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, see . +*********************************************************************/ + +#ifndef KWIN_SHOWPAINT_H +#define KWIN_SHOWPAINT_H + +#include + +namespace KWin +{ + +class ShowPaintEffect : public Effect +{ + Q_OBJECT + +public: + void paintScreen(int mask, QRegion region, ScreenPaintData &data) override; + void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data) override; + +private: + void paintGL(const QMatrix4x4 &projection); + void paintXrender(); + void paintQPainter(); + + QRegion m_painted; // what's painted in one pass + int m_colorIndex = 0; +}; + +} // namespace KWin + +#endif diff --git a/effects/slide/CMakeLists.txt b/effects/slide/CMakeLists.txt new file mode 100644 index 0000000..fa35bef --- /dev/null +++ b/effects/slide/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_slide_config_SRCS slide_config.cpp) +ki18n_wrap_ui(kwin_slide_config_SRCS slide_config.ui) +qt5_add_dbus_interface(kwin_slide_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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 + Qt5::DBus + KF5::ConfigWidgets + KF5::I18n + KF5::Service +) + +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..9e0e8a4 --- /dev/null +++ b/effects/slide/slide.cpp @@ -0,0 +1,545 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak +Copyright (C) 2008 Lucas Murray +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +// own +#include "slide.h" + +// KConfigSkeleton +#include "slideconfig.h" + +// KWayland +#include +#include +#include + +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); +} + +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, QRegion region, ScreenPaintData &data) +{ + if (!m_active) { + effects->paintScreen(mask, region, data); + return; + } + + 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) +{ + if (m_active) { + 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 (m_active && 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::shouldForceBlur(const EffectWindow *w) const +{ + // While there is an active fullscreen effect, the blur effect + // tends to do nothing, i.e. it doesn't blur behind windows. + // So, we should force the blur effect to blur by setting + // WindowForceBlurRole. + + if (w->data(WindowForceBlurRole).toBool()) { + return false; + } + + if (w->data(WindowBlurBehindRole).isValid()) { + return true; + } + + if (w->decorationHasAlpha() && effects->decorationSupportsBlurBehind()) { + return true; + } + + // FIXME: it should be something like this: + // if (surf) { + // return !surf->blur().isNull(); + // } + const KWayland::Server::SurfaceInterface *surf = w->surface(); + if (surf && surf->blur()) { + return true; + } + + // TODO: make it X11-specific(check _KDE_NET_WM_BLUR_BEHIND_REGION) + // or delete it in the future + return w->hasAlpha(); +} + +bool SlideEffect::shouldForceBackgroundContrast(const EffectWindow *w) const +{ + // While there is an active fullscreen effect, the background + // contrast effect tends to do nothing, i.e. it doesn't change + // contrast. So, we should force the background contrast effect + // to change contrast by setting WindowForceBackgroundContrastRole. + + if (w->data(WindowForceBackgroundContrastRole).toBool()) { + return false; + } + + if (w->data(WindowBackgroundContrastRole).isValid()) { + return true; + } + + // FIXME: it should be something like this: + // if (surf) { + // return !surf->contrast().isNull(); + // } + const KWayland::Server::SurfaceInterface *surf = w->surface(); + if (surf && surf->contrast()) { + return true; + } + + // TODO: make it X11-specific(check _KDE_NET_WM_BACKGROUND_CONTRAST_REGION) + // or delete it in the future + return w->hasAlpha() + && w->isOnAllDesktops() + && (w->isDock() || w->keepAbove()); +} + +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 (shouldForceBlur(w)) { + w->setData(WindowForceBlurRole, QVariant(true)); + m_forcedRoles.blur << w; + } + if (shouldForceBackgroundContrast(w)) { + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + m_forcedRoles.backgroundContrast << w; + } + if (shouldElevate(w)) { + effects->setElevatedWindow(w, true); + m_elevatedWindows << w; + } + } + + 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() +{ + for (EffectWindow *w : m_forcedRoles.blur) { + w->setData(WindowForceBlurRole, QVariant()); + } + m_forcedRoles.blur.clear(); + + for (EffectWindow *w : m_forcedRoles.backgroundContrast) { + w->setData(WindowForceBackgroundContrastRole, QVariant()); + } + m_forcedRoles.backgroundContrast.clear(); + + 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 (shouldForceBlur(w)) { + w->setData(WindowForceBlurRole, QVariant(true)); + m_forcedRoles.blur << w; + } + if (shouldForceBackgroundContrast(w)) { + w->setData(WindowForceBackgroundContrastRole, QVariant(true)); + m_forcedRoles.backgroundContrast << w; + } + if (shouldElevate(w)) { + effects->setElevatedWindow(w, true); + m_elevatedWindows << w; + } +} + +void SlideEffect::windowDeleted(EffectWindow *w) +{ + if (!m_active) { + return; + } + if (w == m_movingWindow) { + m_movingWindow = nullptr; + } + m_forcedRoles.blur.removeAll(w); + m_forcedRoles.backgroundContrast.removeAll(w); + 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..e3c91b8 --- /dev/null +++ b/effects/slide/slide.h @@ -0,0 +1,149 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak +Copyright (C) 2008 Lucas Murray +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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(); + + void reconfigure(ReconfigureFlags) override; + + void prePaintScreen(ScreenPrePaintData &data, int time) override; + void paintScreen(int mask, QRegion region, 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 shouldForceBlur(const EffectWindow *w) const; + bool shouldForceBackgroundContrast(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; + + struct { + EffectWindowList blur; + EffectWindowList backgroundContrast; + } m_forcedRoles; + + 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..8f6d6ab --- /dev/null +++ b/effects/slide/slide_config.cpp @@ -0,0 +1,63 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017, 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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..1774f68 --- /dev/null +++ b/effects/slide/slide_config.desktop @@ -0,0 +1,73 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_slide_config +X-KDE-ParentComponents=slide + +Name=Slide +Name[ar]=أزْلِق +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]=Slide +Name[is]=Renna til +Name[it]=Scivola +Name[ja]=スライド +Name[kk]=Сырғанату +Name[km]=ស្លាយ +Name[kn]=ಜಾರು +Name[ko]=슬라이드 +Name[ku]=Xîş Bike +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[tg]=Слайд +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..f6286b1 --- /dev/null +++ b/effects/slide/slide_config.h @@ -0,0 +1,47 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017, 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + + +#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(); + + void save(); + +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..1564954 --- /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..757b436 --- /dev/null +++ b/effects/slideback/slideback.cpp @@ -0,0 +1,347 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Michael Zanetti + +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, see . +*********************************************************************/ + +#include "slideback.h" + +namespace KWin +{ + +SlideBackEffect::SlideBackEffect() +{ + m_tabboxActive = 0; + m_justMapped = m_upmostWindow = NULL; + connect(effects, SIGNAL(windowAdded(KWin::EffectWindow*)), SLOT(slotWindowAdded(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowDeleted(KWin::EffectWindow*)), SLOT(slotWindowDeleted(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowUnminimized(KWin::EffectWindow*)), SLOT(slotWindowUnminimized(KWin::EffectWindow*))); + connect(effects, SIGNAL(tabBoxAdded(int)), SLOT(slotTabBoxAdded())); + connect(effects, SIGNAL(stackingOrderChanged()), SLOT(slotStackingOrderChanged())); + connect(effects, SIGNAL(tabBoxClosed()), SLOT(slotTabBoxClosed())); +} + +static inline bool windowsShareDesktop(EffectWindow *w1, EffectWindow *w2) +{ + return w1->isOnAllDesktops() || w2->isOnAllDesktops() || w1->desktop() == w2->desktop(); +} + + +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 = 0; + 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) && windowsShareDesktop(tmp, w)) { + // 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 = 0; + if (w == m_justMapped) + m_justMapped = 0; + 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->isCurrentTab() && 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..c71502e --- /dev/null +++ b/effects/slideback/slideback.h @@ -0,0 +1,80 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Michael Zanetti + +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, see . +*********************************************************************/ + +#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(); + + virtual void prePaintWindow(EffectWindow *w, WindowPrePaintData &data, int time); + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual void postPaintWindow(EffectWindow* w); + + virtual void prePaintScreen(ScreenPrePaintData &data, int time); + virtual void postPaintScreen(); + virtual bool isActive() const; + + 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..b3de9b5 --- /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..066b2a3 --- /dev/null +++ b/effects/slidingpopups/slidingpopups.cpp @@ -0,0 +1,464 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Marco Martin notmart@gmail.com +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#include "slidingpopups.h" +#include "slidingpopupsconfig.h" + +#include +#include + +#include +#include +#include + +namespace KWin +{ + +SlidingPopupsEffect::SlidingPopupsEffect() +{ + initConfig(); + KWayland::Server::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(), w->desktop()); + 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, &KWayland::Server::SurfaceInterface::slideOnShowHideChanged, this, [this, surf] { + slotWaylandSlideOnShowChanged(effects->findWindow(surf)); + }); + } + + slideIn(w); +} + +void SlidingPopupsEffect::slotWindowDeleted(EffectWindow *w) +{ + m_animations.remove(w); + m_animationsData.remove(w); + effects->addRepaint(w->expandedGeometry()); +} + +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; + } + + KWayland::Server::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 KWayland::Server::SlideInterface::Location::Top: + animData.location = Location::Top; + break; + case KWayland::Server::SlideInterface::Location::Left: + animData.location = Location::Left; + break; + case KWayland::Server::SlideInterface::Location::Right: + animData.location = Location::Right; + break; + case KWayland::Server::SlideInterface::Location::Bottom: + default: + animData.location = Location::Bottom; + break; + } + animData.slideLength = 0; + animData.slideInDuration = m_slideInDuration; + animData.slideOutDuration = m_slideOutDuration; + + setupAnimData(w); + } +} + +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.reset(); + animation.timeLine.setDirection(TimeLine::Forward); + animation.timeLine.setDuration((*dataIt).slideInDuration); + animation.timeLine.setEasingCurve(QEasingCurve::InOutSine); + + 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.reset(); + animation.timeLine.setDirection(TimeLine::Backward); + animation.timeLine.setDuration((*dataIt).slideOutDuration); + animation.timeLine.setEasingCurve(QEasingCurve::InOutSine); + + 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..32e8fb5 --- /dev/null +++ b/effects/slidingpopups/slidingpopups.h @@ -0,0 +1,115 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Marco Martin notmart@gmail.com +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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; + +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); + + 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..6b7b980 --- /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..6a22c20 --- /dev/null +++ b/effects/snaphelper/snaphelper.cpp @@ -0,0 +1,237 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +#include "snaphelper.h" + +#include +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#include +#endif +#include + +namespace KWin +{ + +SnapHelperEffect::SnapHelperEffect() + : m_active(false) + , m_window(NULL) +{ + m_timeline.setCurveShape(QTimeLine::LinearCurve); + reconfigure(ReconfigureAll); + connect(effects, SIGNAL(windowClosed(KWin::EffectWindow*)), this, SLOT(slotWindowClosed(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowStartUserMovedResized(KWin::EffectWindow*)), this, SLOT(slotWindowStartUserMovedResized(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowFinishUserMovedResized(KWin::EffectWindow*)), this, SLOT(slotWindowFinishUserMovedResized(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowGeometryShapeChanged(KWin::EffectWindow*,QRect)), this, SLOT(slotWindowResized(KWin::EffectWindow*,QRect))); +} + +SnapHelperEffect::~SnapHelperEffect() +{ +} + +void SnapHelperEffect::reconfigure(ReconfigureFlags) +{ + m_timeline.setDuration(animationTime(250)); +} + +void SnapHelperEffect::prePaintScreen(ScreenPrePaintData &data, int time) +{ + double oldValue = m_timeline.currentValue(); + if (m_active) + m_timeline.setCurrentTime(m_timeline.currentTime() + time); + else + m_timeline.setCurrentTime(m_timeline.currentTime() - time); + if (oldValue != m_timeline.currentValue()) + effects->addRepaintFull(); + effects->prePaintScreen(data, time); +} + +void SnapHelperEffect::paintScreen(int mask, QRegion region, ScreenPaintData &data) +{ + effects->paintScreen(mask, region, data); + if (m_timeline.currentValue() != 0.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; + color.setRedF(0.5); + color.setGreenF(0.5); + color.setBlueF(0.5); + color.setAlphaF(m_timeline.currentValue() * 0.5); + vbo->setColor(color); + glLineWidth(4.0); + QVector verts; + verts.reserve(effects->numScreens() * 24); + for (int i = 0; i < effects->numScreens(); ++i) { + const QRect& rect = effects->clientArea(ScreenArea, i, 0); + int midX = rect.x() + rect.width() / 2; + int midY = rect.y() + rect.height() / 2 ; + int halfWidth = m_window->width() / 2; + int halfHeight = m_window->height() / 2; + + // Center lines + verts << rect.x() + rect.width() / 2 << rect.y(); + verts << rect.x() + rect.width() / 2 << rect.y() + rect.height(); + verts << rect.x() << rect.y() + rect.height() / 2; + verts << rect.x() + rect.width() << rect.y() + rect.height() / 2; + + // Window outline + // The +/- 2 is to prevent line overlap + verts << midX - halfWidth + 2 << midY - halfHeight; + verts << midX + halfWidth + 2 << midY - halfHeight; + verts << midX + halfWidth << midY - halfHeight + 2; + verts << midX + halfWidth << midY + halfHeight + 2; + verts << midX + halfWidth - 2 << midY + halfHeight; + verts << midX - halfWidth - 2 << midY + halfHeight; + verts << midX - halfWidth << midY + halfHeight - 2; + verts << midX - halfWidth << midY - halfHeight - 2; + } + vbo->setData(verts.count() / 2, 2, verts.data(), NULL); + 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 ); + int midX = rect.x() + rect.width() / 2; + int midY = rect.y() + rect.height() / 2 ; + int halfWidth = m_window->width() / 2; + int halfHeight = m_window->height() / 2; + + xcb_rectangle_t rects[6]; + // Center lines + rects[0].x = rect.x() + rect.width() / 2 - 2; + rects[0].y = rect.y(); + rects[0].width = 4; + rects[0].height = rect.height(); + rects[1].x = rect.x(); + rects[1].y = rect.y() + rect.height() / 2 - 2; + rects[1].width = rect.width(); + rects[1].height = 4; + + // Window outline + // The +/- 4 is to prevent line overlap + rects[2].x = midX - halfWidth + 4; + rects[2].y = midY - halfHeight; + rects[2].width = 2*halfWidth - 4; + rects[2].height = 4; + rects[3].x = midX + halfWidth - 4; + rects[3].y = midY - halfHeight + 4; + rects[3].width = 4; + rects[3].height = 2*halfHeight - 4; + rects[4].x = midX - halfWidth; + rects[4].y = midY + halfHeight - 4; + rects[4].width = 2*halfWidth - 4; + rects[4].height = 4; + rects[5].x = midX - halfWidth; + rects[5].y = midY - halfHeight; + rects[5].width = 4; + rects[5].height = 2*halfHeight - 4; + + xcb_render_fill_rectangles(xcbConnection(), XCB_RENDER_PICT_OP_OVER, effects->xrenderBufferPicture(), + preMultiply(QColor(128, 128, 128, m_timeline.currentValue()*128)), 6, rects); + } +#endif + } + if (effects->compositingType() == QPainterCompositing) { + QPainter *painter = effects->scenePainter(); + painter->save(); + QColor color; + color.setRedF(0.5); + color.setGreenF(0.5); + color.setBlueF(0.5); + color.setAlphaF(m_timeline.currentValue() * 0.5); + QPen pen(color); + pen.setWidth(4); + 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 windowRect(rect.center(), m_window->geometry().size()); + painter->drawRect(windowRect.translated(-windowRect.width()/2, -windowRect.height()/2)); + } + painter->restore(); + } + } else if (m_window && !m_active) { + if (m_window->isDeleted()) + m_window->unrefWindow(); + m_window = NULL; + } +} + +void SnapHelperEffect::slotWindowClosed(EffectWindow* w) +{ + if (m_window == w) { + m_window->refWindow(); + m_active = false; + } +} + +void SnapHelperEffect::slotWindowStartUserMovedResized(EffectWindow *w) +{ + if (w->isMovable()) { + m_active = true; + m_window = w; + effects->addRepaintFull(); + } +} + +void SnapHelperEffect::slotWindowFinishUserMovedResized(EffectWindow *w) +{ + Q_UNUSED(w) + if (m_active) { + m_active = false; + effects->addRepaintFull(); + } +} + +void SnapHelperEffect::slotWindowResized(KWin::EffectWindow *w, const QRect &oldRect) +{ + if (w == m_window) { + QRect r(oldRect); + for (int i = 0; i < effects->numScreens(); ++i) { + r.moveCenter(effects->clientArea( ScreenArea, i, 0 ).center()); + effects->addRepaint(r); + } + } +} + +bool SnapHelperEffect::isActive() const +{ + return m_active || m_timeline.currentValue() != 0.0; +} + +} // namespace diff --git a/effects/snaphelper/snaphelper.h b/effects/snaphelper/snaphelper.h new file mode 100644 index 0000000..de3096f --- /dev/null +++ b/effects/snaphelper/snaphelper.h @@ -0,0 +1,59 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +#ifndef KWIN_SNAPHELPER_H +#define KWIN_SNAPHELPER_H + +#include +#include + +namespace KWin +{ + +class SnapHelperEffect + : public Effect +{ + Q_OBJECT +public: + SnapHelperEffect(); + ~SnapHelperEffect(); + + virtual void reconfigure(ReconfigureFlags); + + virtual void prePaintScreen(ScreenPrePaintData &data, int time); + void paintScreen(int mask, QRegion region, ScreenPaintData &data) override; + virtual bool isActive() const; + +public Q_SLOTS: + void slotWindowClosed(KWin::EffectWindow *w); + void slotWindowStartUserMovedResized(KWin::EffectWindow *w); + void slotWindowFinishUserMovedResized(KWin::EffectWindow *w); + void slotWindowResized(KWin::EffectWindow *w, const QRect &r); + +private: + bool m_active; + EffectWindow* m_window; + QTimeLine m_timeline; + //GC m_gc; +}; + +} // namespace + +#endif diff --git a/effects/startupfeedback/CMakeLists.txt b/effects/startupfeedback/CMakeLists.txt new file mode 100644 index 0000000..19a3783 --- /dev/null +++ b/effects/startupfeedback/CMakeLists.txt @@ -0,0 +1,10 @@ +####################################### +# Effect + +# Source files +set( kwin4_effect_builtins_sources ${kwin4_effect_builtins_sources} + startupfeedback/startupfeedback.cpp + ) + +####################################### +# Config diff --git a/effects/startupfeedback/data/blinking-startup-fragment.glsl b/effects/startupfeedback/data/blinking-startup-fragment.glsl new file mode 100644 index 0000000..3229f58 --- /dev/null +++ b/effects/startupfeedback/data/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/startupfeedback.cpp b/effects/startupfeedback/startupfeedback.cpp new file mode 100644 index 0000000..08e1e2d --- /dev/null +++ b/effects/startupfeedback/startupfeedback.cpp @@ -0,0 +1,402 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2010 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "startupfeedback.h" +// Qt +#include +#include +#include +#include +#include +#include +// KDE +#include +#include +#include +#include +#include +#include +// KWin +#include + +// based on StartupId in KRunner by Lubos Lunak +// Copyright (C) 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(0) + , m_type(BouncingFeedback) + , m_blinkingShader(0) + , m_cursorSize(0) +{ + for (int i = 0; i < 5; ++i) { + m_bouncingTextures[i] = 0; + } + if (KWindowSystem::isPlatformX11()) { + m_selection = new KSelectionOwner("_KDE_STARTUP_FEEDBACK", xcbConnection(), x11RootWindow(), this); + m_selection->claim(true); + } + connect(m_startupInfo, SIGNAL(gotNewStartup(KStartupInfoId,KStartupInfoData)), SLOT(gotNewStartup(KStartupInfoId,KStartupInfoData))); + connect(m_startupInfo, SIGNAL(gotRemoveStartup(KStartupInfoId,KStartupInfoData)), SLOT(gotRemoveStartup(KStartupInfoId,KStartupInfoData))); + connect(m_startupInfo, SIGNAL(gotStartupChange(KStartupInfoId,KStartupInfoData)), SLOT(gotStartupChange(KStartupInfoId,KStartupInfoData))); + connect(effects, SIGNAL(mouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers)), + this, SLOT(slotMouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers))); + 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) + KConfig conf(QStringLiteral("klaunchrc"), KConfig::NoGlobals); + KConfigGroup c = conf.group("FeedbackStyle"); + const bool busyCursor = c.readEntry("BusyCursor", true); + + c = conf.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, QRegion region, 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; + // 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(KIconLoader::Small) / 16.0; + QPixmap iconPixmap = KIconLoader::global()->loadIcon(icon, KIconLoader::Small, 0, + KIconLoader::DefaultState, QStringList(), 0, true); // return null pixmap if not found + if (iconPixmap.isNull()) + iconPixmap = SmallIcon(QStringLiteral("system-run")); + prepareTextures(iconPixmap); + auto readCursorSize = []() -> int { + // read details about the mouse-cursor theme define per default + KConfigGroup mousecfg(effects->inputConfig(), "Mouse"); + QString size = mousecfg.readEntry("cursorSize", QString()); + + // fetch a reasonable size for the cursor-theme image + bool ok; + int cursorSize = size.toInt(&ok); + if (!ok) + cursorSize = QApplication::style()->pixelMetric(QStyle::PM_LargeIconSize); + return cursorSize; + }; + m_cursorSize = readCursorSize(); + 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] = 0; + } + break; + case BlinkingFeedback: + case PassiveFeedback: + delete m_texture; + m_texture = 0; + 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 = 0; + 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..96ed0d8 --- /dev/null +++ b/effects/startupfeedback/startupfeedback.h @@ -0,0 +1,93 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2010 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef KWIN_STARTUPFEEDBACK_H +#define KWIN_STARTUPFEEDBACK_H +#include +#include +#include + +class KSelectionOwner; +namespace KWin +{ +class GLTexture; + +class StartupFeedbackEffect + : public Effect +{ + Q_OBJECT + Q_PROPERTY(int type READ type) +public: + StartupFeedbackEffect(); + virtual ~StartupFeedbackEffect(); + + virtual void reconfigure(ReconfigureFlags flags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual void postPaintScreen(); + virtual bool isActive() const; + + 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; +}; +} // namespace + +#endif diff --git a/effects/thumbnailaside/CMakeLists.txt b/effects/thumbnailaside/CMakeLists.txt new file mode 100644 index 0000000..ddcc7f0 --- /dev/null +++ b/effects/thumbnailaside/CMakeLists.txt @@ -0,0 +1,25 @@ +####################################### +# Config +set(kwin_thumbnailaside_config_SRCS thumbnailaside_config.cpp) +ki18n_wrap_ui(kwin_thumbnailaside_config_SRCS thumbnailaside_config.ui) +qt5_add_dbus_interface(kwin_thumbnailaside_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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::Service + KF5::XmlGui +) + +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..2e58ea9 --- /dev/null +++ b/effects/thumbnailaside/thumbnailaside.cpp @@ -0,0 +1,192 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +#include "thumbnailaside.h" +// KConfigSkeleton +#include "thumbnailasideconfig.h" + +#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, SIGNAL(triggered(bool)), this, SLOT(toggleCurrentThumbnail())); + + connect(effects, SIGNAL(windowClosed(KWin::EffectWindow*)), this, SLOT(slotWindowClosed(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowGeometryShapeChanged(KWin::EffectWindow*,QRect)), this, SLOT(slotWindowGeometryShapeChanged(KWin::EffectWindow*,QRect))); + connect(effects, SIGNAL(windowDamaged(KWin::EffectWindow*,QRect)), this, SLOT(slotWindowDamaged(KWin::EffectWindow*,QRect))); + connect(effects, SIGNAL(screenLockingChanged(bool)), SLOT(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, QRegion region, ScreenPaintData& data) +{ + painted = QRegion(); + effects->paintScreen(mask, region, data); + foreach (const Data & d, windows) { + if (painted.intersects(d.rect)) { + WindowPaintData data(d.window); + 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::slotWindowGeometryShapeChanged(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 == NULL) + 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..bfe4931 --- /dev/null +++ b/effects/thumbnailaside/thumbnailaside.h @@ -0,0 +1,91 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Lubos Lunak +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +/* + + 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(); + virtual void reconfigure(ReconfigureFlags); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual void paintWindow(EffectWindow *w, int mask, QRegion region, WindowPaintData &data); + + // 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 slotWindowGeometryShapeChanged(KWin::EffectWindow *w, const QRect &old); + void slotWindowDamaged(KWin::EffectWindow* w, const QRect& damage); + virtual bool isActive() const; + 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..89eccbe --- /dev/null +++ b/effects/thumbnailaside/thumbnailaside_config.cpp @@ -0,0 +1,101 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +#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, SIGNAL(keyChange()), this, SLOT(changed())); + + 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..7422a66 --- /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[be@latin]=Padhlady akon +Name[bg]=Странични миниатюри +Name[bs]=Sličica postrance +Name[ca]=Miniatures de 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]=Thumbnail Aside +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ūros pakrašty +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[tg]=Миниатюры сбоку +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..6756672 --- /dev/null +++ b/effects/thumbnailaside/thumbnailaside_config.h @@ -0,0 +1,56 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Christian Nitschkowski + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + virtual ~ThumbnailAsideEffectConfig(); + + virtual void save(); + +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..71a7469 --- /dev/null +++ b/effects/touchpoints/touchpoints.cpp @@ -0,0 +1,325 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2012 Filip Wieladek +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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(quint32 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(quint32 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(quint32 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, QRegion region, 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(), NULL); + 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..1529651 --- /dev/null +++ b/effects/touchpoints/touchpoints.h @@ -0,0 +1,99 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2012 Filip Wieladek +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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(); + void prePaintScreen(ScreenPrePaintData& data, int time) override; + void paintScreen(int mask, QRegion region, ScreenPaintData& data) override; + void postPaintScreen() override; + bool isActive() const override; + bool touchDown(quint32 id, const QPointF &pos, quint32 time) override; + bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override; + bool touchUp(quint32 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..471b9ab --- /dev/null +++ b/effects/trackmouse/CMakeLists.txt @@ -0,0 +1,33 @@ +####################################### +# 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) +qt5_add_dbus_interface(kwin_trackmouse_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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::Service + KF5::XmlGui +) + +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 +Copyright (C) 2010 Jorge Mata +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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] = 0; +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + m_picture[0] = m_picture[1] = 0; + 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, SIGNAL(triggered(bool)), this, SLOT(toggle())); + + connect(effects, SIGNAL(mouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers)), + SLOT(slotMouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers))); + reconfigure(ReconfigureAll); +} + +TrackMouseEffect::~TrackMouseEffect() +{ + if (m_mousePolling) + effects->stopMousePolling(); + for (int i = 0; i < 2; ++i) { + delete m_texture[i]; m_texture[i] = 0; +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + delete m_picture[i]; m_picture[i] = 0; +#endif + } +} + +void TrackMouseEffect::reconfigure(ReconfigureFlags) +{ + m_modifiers = 0; + 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, QRegion region, 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, NULL); + 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..f2eb602 --- /dev/null +++ b/effects/trackmouse/trackmouse.h @@ -0,0 +1,87 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2010 Jorge Mata +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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(); + virtual ~TrackMouseEffect(); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual void postPaintScreen(); + virtual void reconfigure(ReconfigureFlags); + virtual bool isActive() const; + + // 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..94d3211 --- /dev/null +++ b/effects/trackmouse/trackmouse_config.cpp @@ -0,0 +1,124 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2010 Jorge Mata + +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, see . +*********************************************************************/ + +#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, SIGNAL(keySequenceChanged(QKeySequence)), + SLOT(shortcutChanged(QKeySequence))); + + 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..c287bf9 --- /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[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]=Track 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[tg]=Отслеживание мыши +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..56f9a60 --- /dev/null +++ b/effects/trackmouse/trackmouse_config.h @@ -0,0 +1,61 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2010 Jorge Mata + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + ~TrackMouseEffectConfig(); + +public Q_SLOTS: + virtual void save(); + virtual void load(); + virtual void defaults(); +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/CMakeLists.txt b/effects/translucency/CMakeLists.txt new file mode 100644 index 0000000..1242620 --- /dev/null +++ b/effects/translucency/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(package) diff --git a/effects/translucency/package/CMakeLists.txt b/effects/translucency/package/CMakeLists.txt new file mode 100644 index 0000000..2607a15 --- /dev/null +++ b/effects/translucency/package/CMakeLists.txt @@ -0,0 +1,6 @@ +install(DIRECTORY contents DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_translucency) +install(FILES metadata.desktop DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_translucency) + +install(FILES metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_effect_translucency.desktop) diff --git a/effects/translucency/package/contents/code/main.js b/effects/translucency/package/contents/code/main.js new file mode 100644 index 0000000..69c0511 --- /dev/null +++ b/effects/translucency/package/contents/code/main.js @@ -0,0 +1,235 @@ +/******************************************************************** + This file is part of the KDE project. + + Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +/*global effect, effects, animate, cancel, set, animationTime, Effect, QEasingCurve */ +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 () { + "use strict"; + 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) { + "use strict"; + 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) { + "use strict"; + if (window.translucencyWindowTypeAnimation !== undefined) { + cancel(window.translucencyWindowTypeAnimation); + window.translucencyWindowTypeAnimation = undefined; + } + if (window.translucencyInactiveAnimation !== undefined) { + cancel(window.translucencyInactiveAnimation); + window.translucencyInactiveAnimation = undefined; + } + }, + moveResize: { + start: function (window) { + "use strict"; + 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) { + "use strict"; + 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) { + "use strict"; + 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) { + "use strict"; + var ids; + if (translucencyEffect.settings.inactive === 100) { + return; + } + if (window === null) { + return; + } + if (window === effects.activeWindow || + 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 () { + "use strict"; + 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 () { + "use strict"; + 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..3146320 --- /dev/null +++ b/effects/translucency/package/metadata.desktop @@ -0,0 +1,172 @@ +[Desktop Entry] +Name=Translucency +Name[af]=Deursigtigheid +Name[ar]=شبه الشفافية +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]=Translucency +Name[is]=Hálfgegnsæi +Name[it]=Translucenza +Name[ja]=半透明 +Name[ka]=ნახევრადგამჭირვალეობა +Name[kk]=Мөлдірлік +Name[km]=ភាព​ល្អក់ +Name[kn]=ಪಾರದೀಪಕತೆ (ಟ್ರಾನ್ಸಲುಸೆಂಸಿ) +Name[ko]=반투명 +Name[lt]=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[tg]=Полупрозрачность +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[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 jendela translusens di bawah kondisi 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 padaro langus permatomus +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]=Prześwitywanie okien 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-Depends= +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/CMakeLists.txt b/effects/windowaperture/CMakeLists.txt new file mode 100644 index 0000000..28f40b3 --- /dev/null +++ b/effects/windowaperture/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory( package ) diff --git a/effects/windowaperture/package/CMakeLists.txt b/effects/windowaperture/package/CMakeLists.txt new file mode 100644 index 0000000..55ee40d --- /dev/null +++ b/effects/windowaperture/package/CMakeLists.txt @@ -0,0 +1,6 @@ +install(DIRECTORY contents DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_windowaperture) +install(FILES metadata.desktop DESTINATION ${DATA_INSTALL_DIR}/${KWIN_NAME}/effects/kwin4_effect_windowaperture) + +install(FILES metadata.desktop + DESTINATION ${SERVICES_INSTALL_DIR}/${KWIN_NAME} + RENAME kwin4_effect_windowaperture.desktop) diff --git a/effects/windowaperture/package/contents/code/main.js b/effects/windowaperture/package/contents/code/main.js new file mode 100644 index 0000000..8c55e84 --- /dev/null +++ b/effects/windowaperture/package/contents/code/main.js @@ -0,0 +1,204 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2015 Thomas Lübking + +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, see . +*********************************************************************/ +/*global effect, effects, animate, animationTime, Effect, QEasingCurve */ + +var badBadWindowsEffect = { + duration: animationTime(250), + loadConfig: function () { + "use strict"; + badBadWindowsEffect.duration = animationTime(250); + }, + offToCorners: function (showing) { + "use strict"; + 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.InOutQuad, + 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.InOutQuad, + animations: [{ + type: Effect.Position, + sourceAnchor: anchor, + from: { value1: tx, value2: ty } + },{ + type: Effect.Opacity, + from: 0.2 + }] + }); + } + } + } + }, + init: function () { + "use strict"; + 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..a853ee6 --- /dev/null +++ b/effects/windowaperture/package/metadata.desktop @@ -0,0 +1,95 @@ +[Desktop Entry] +Name=Window Aperture +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 Aperture +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 window into the corners while showing desktop +Comment[ca]=Mou la finestra cap a les cantonades en mostrar l'escriptori +Comment[ca@valencia]=Mou la finestra cap a les cantonades en mostrar l'escriptori +Comment[da]=Flyt vinduer til hjørnerne når skrivebordet vises +Comment[de]=Verschiebt Fenster bei der Anzeige der Arbeitsfläche in die Ecken. +Comment[el]=Μετακίνηση παραθύρου στις γωνίες κατά την εμφάνιση της επιφάνειας εργασίας +Comment[en_GB]=Move window into the corners while showing desktop +Comment[es]=Mover las ventanas a las esquinas cuando se muestra el escritorio +Comment[et]=Akende viimine nurkadesse töölauda näidates +Comment[eu]=Mugitu leihoa izkinetan mahaigana erakutsi bitartean +Comment[fi]=Siirrä ikkunat näytön kulmiin työpöydän näyttämisen ajaksi +Comment[fr]=Déplace les fenêtres dans les coins en affichant le bureau +Comment[gl]=Mover a xanela ás esquinas mentres se mostra o escritorio. +Comment[hu]=A sarkokba mozgatja az ablakokat az asztal megjelenítése közben +Comment[ia]=Move fenestra a le angulos quando monstra scriptorio +Comment[id]=Pindah jendela ke pojok ketika menampilkan desktop +Comment[it]=Sposta la finestra negli angoli quando mostri il desktop +Comment[ja]=デスクトップを表示している間に、ウインドウを角に移動させます +Comment[ko]=데스크톱을 보일 때 창을 모서리로 이동 +Comment[nb]=Flytt vindu til hjørnene mens skrivebordet vises +Comment[nl]=Schuif venster in de hoeken tijdens het tonen van het bureaublad +Comment[nn]=Flytt vindauge til hjørna mens skrivebordet vert vist +Comment[pl]=Rozsuwa okna w narożniki, podczas pokazywania pulpitu +Comment[pt]=Mover a janela para os cantos, enquanto mostra o ecrã +Comment[pt_BR]=Move a janela para os cantos, enquanto mostra a área de trabalho +Comment[ru]=Для просмотра рабочего стола окна временно разлетаются в разные стороны +Comment[sk]=Posun okna do rohov počas zobrazenia plochy +Comment[sl]=Premakne okno v kote med prikazovanjem namizja +Comment[sr]=Помера прозор у углове при показивању површи +Comment[sr@ijekavian]=Помера прозор у углове при показивању површи +Comment[sr@ijekavianlatin]=Pomera prozor u uglove pri pokazivanju površi +Comment[sr@latin]=Pomera prozor u uglove pri pokazivanju površi +Comment[sv]=Flytta fönster till hörnen medan skrivbordet visas +Comment[tr]=Masaüstünü gösterirken pencereyi köşelere taşı +Comment[uk]=Пересувати вікна в куток на час перегляду стільниці +Comment[x-test]=xxMove window into the corners while showing 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_windowaperture +X-KDE-PluginInfo-Version=0.1.0 +X-KDE-PluginInfo-Category=Show Desktop Animation +X-KDE-PluginInfo-Depends= +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..de76562 --- /dev/null +++ b/effects/windowgeometry/CMakeLists.txt @@ -0,0 +1,25 @@ +####################################### +# Config +set(kwin_windowgeometry_config_SRCS windowgeometry_config.cpp) +ki18n_wrap_ui(kwin_windowgeometry_config_SRCS windowgeometry_config.ui) +qt5_add_dbus_interface(kwin_windowgeometry_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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::Service + KF5::XmlGui +) + +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..8d66ead --- /dev/null +++ b/effects/windowgeometry/windowgeometry.cpp @@ -0,0 +1,237 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2010 Thomas Lübking + +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, see . +*********************************************************************/ + +#include "windowgeometry.h" +// KConfigSkeleton +#include "windowgeometryconfig.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace KWin; + +WindowGeometry::WindowGeometry() +{ + initConfig(); + iAmActivated = true; + iAmActive = false; + myResizeWindow = 0L; +#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, SIGNAL(triggered(bool)), this, SLOT(toggle())); + + connect(effects, SIGNAL(windowStartUserMovedResized(KWin::EffectWindow*)), this, SLOT(slotWindowStartUserMovedResized(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowFinishUserMovedResized(KWin::EffectWindow*)), this, SLOT(slotWindowFinishUserMovedResized(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowStepUserMovedResized(KWin::EffectWindow*,QRect)), this, SLOT(slotWindowStepUserMovedResized(KWin::EffectWindow*,QRect))); +} + +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, QRegion region, 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 = 0L; + 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, center, w->desktop()); + 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; +} diff --git a/effects/windowgeometry/windowgeometry.h b/effects/windowgeometry/windowgeometry.h new file mode 100644 index 0000000..6c05550 --- /dev/null +++ b/effects/windowgeometry/windowgeometry.h @@ -0,0 +1,73 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2010 Thomas Lübking + +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, see . +*********************************************************************/ + +#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(); + + inline bool provides(Effect::Feature ef) { + return ef == Effect::GeometryTip; + } + void reconfigure(ReconfigureFlags); + void paintScreen(int mask, QRegion region, ScreenPaintData &data); + virtual bool isActive() const; + + 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..3544e4c --- /dev/null +++ b/effects/windowgeometry/windowgeometry_config.cpp @@ -0,0 +1,95 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2010 Thomas Lübking + +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, see . +*********************************************************************/ + +#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, SIGNAL(keyChange()), this, SLOT(changed())); + + 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..f17d310 --- /dev/null +++ b/effects/windowgeometry/windowgeometry_config.desktop @@ -0,0 +1,63 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule + +X-KDE-Library=kwin_windowgeometry_config +X-KDE-ParentComponents=windowgeometry + +Name=WindowGeometry +Name[bg]=Геометрия на прозореца +Name[bs]=Geometrija prozora +Name[ca]=WindowGeometry +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]=WindowGeometry +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..eaa49c4 --- /dev/null +++ b/effects/windowgeometry/windowgeometry_config.h @@ -0,0 +1,57 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2010 Thomas Lübking + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + ~WindowGeometryConfig(); + +public Q_SLOTS: + void save(); + void defaults(); + +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..6d9d4ac --- /dev/null +++ b/effects/wobblywindows/CMakeLists.txt @@ -0,0 +1,24 @@ +####################################### +# Config +set(kwin_wobblywindows_config_SRCS wobblywindows_config.cpp) +ki18n_wrap_ui(kwin_wobblywindows_config_SRCS wobblywindows_config.ui) +qt5_add_dbus_interface(kwin_wobblywindows_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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 + Qt5::DBus + KF5::ConfigWidgets + KF5::I18n + KF5::Service +) + +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..68ce567 --- /dev/null +++ b/effects/wobblywindows/wobblywindows.cpp @@ -0,0 +1,1245 @@ +/***************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2008 Cédric Borgese + +You can Freely distribute this program under the GNU General Public +License. See the file "COPYING" for the exact licensing terms. +******************************************************************/ + + +#include "wobblywindows.h" +#include "wobblywindowsconfig.h" + +#include + +#define USE_ASSERT +#ifdef USE_ASSERT +#define ASSERT1 assert +#else +#define ASSERT1 +#endif + +//#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; + + bool moveEffectEnabled; + bool openEffectEnabled; + bool closeEffectEnabled; +}; + +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, + true, + false, + false +}; + +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, + true, + false, + false +}; + +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, + true, + false, + false +}; + +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, + true, + false, + false +}; + +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, + true, + false, + false +}; + +static const ParameterSet pset[5] = { set_0, set_1, set_2, set_3, set_4 }; + +WobblyWindowsEffect::WobblyWindowsEffect() +{ + initConfig(); + reconfigure(ReconfigureAll); + connect(effects, SIGNAL(windowAdded(KWin::EffectWindow*)), this, SLOT(slotWindowAdded(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowClosed(KWin::EffectWindow*)), this, SLOT(slotWindowClosed(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowStartUserMovedResized(KWin::EffectWindow*)), this, SLOT(slotWindowStartUserMovedResized(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowStepUserMovedResized(KWin::EffectWindow*,QRect)), this, SLOT(slotWindowStepUserMovedResized(KWin::EffectWindow*,QRect))); + connect(effects, SIGNAL(windowFinishUserMovedResized(KWin::EffectWindow*)), this, SLOT(slotWindowFinishUserMovedResized(KWin::EffectWindow*))); + connect(effects, SIGNAL(windowMaximizedStateChanged(KWin::EffectWindow*,bool,bool)), this, SLOT(slotWindowMaximizeStateChanged(KWin::EffectWindow*,bool,bool))); + + connect(effects, &EffectsHandler::windowDataChanged, this, &WobblyWindowsEffect::cancelWindowGrab); +} + +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_moveEffectEnabled = WobblyWindowsConfig::moveEffect(); + m_openEffectEnabled = WobblyWindowsConfig::openEffect(); + // disable close effect by default for now as it doesn't do what I want. + m_closeEffectEnabled = WobblyWindowsConfig::closeEffect(); + } + + m_moveWobble = WobblyWindowsConfig::moveWobble(); + m_resizeWobble = WobblyWindowsConfig::resizeWobble(); + +#if defined VERBOSE_MODE + qCDebug(KWINEFFECTS) << "Parameters :\n" << + "move : " << m_moveEffectEnabled << ", open : " << m_openEffectEnabled << ", close : " << m_closeEffectEnabled << "\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; + + m_moveEffectEnabled = pset.moveEffectEnabled; + m_openEffectEnabled = pset.openEffectEnabled; + m_closeEffectEnabled = pset.closeEffectEnabled; +} + +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; + + 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 (!m_moveEffectEnabled || 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() || !m_moveEffectEnabled || 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::slotWindowAdded(EffectWindow* w) +{ + if (m_openEffectEnabled && w->data(WindowAddedGrabRole).value() == nullptr) { + if (windows.contains(w)) { + // could this happen ?? + WindowWobblyInfos& wwi = windows[w]; + wobblyOpenInit(wwi); + } else { + WindowWobblyInfos new_wwi; + initWobblyInfo(new_wwi, w->geometry()); + wobblyOpenInit(new_wwi); + windows[w] = new_wwi; + } + } +} + +void WobblyWindowsEffect::slotWindowClosed(EffectWindow* w) +{ + if (windows.contains(w)) { + WindowWobblyInfos& wwi = windows[w]; + if (m_closeEffectEnabled) { + wobblyCloseInit(wwi, w); + w->refWindow(); + } else { + freeWobblyInfo(wwi); + windows.remove(w); + if (windows.isEmpty()) + effects->addRepaintFull(); + } + } else if (m_closeEffectEnabled && w->data(WindowClosedGrabRole).value() == nullptr) { + WindowWobblyInfos new_wwi; + initWobblyInfo(new_wwi, w->geometry()); + wobblyCloseInit(new_wwi, w); + windows[w] = new_wwi; + w->refWindow(); + } +} + +void WobblyWindowsEffect::wobblyOpenInit(WindowWobblyInfos& wwi) const +{ + Pair middle = { (wwi.origin[0].x + wwi.origin[15].x) / 2, (wwi.origin[0].y + wwi.origin[15].y) / 2 }; + + for (unsigned int j = 0; j < 4; ++j) { + for (unsigned int i = 0; i < 4; ++i) { + unsigned int idx = j * 4 + i; + wwi.constraint[idx] = false; + wwi.position[idx].x = (wwi.position[idx].x + 3 * middle.x) / 4; + wwi.position[idx].y = (wwi.position[idx].y + 3 * middle.y) / 4; + } + } + wwi.status = Openning; + wwi.can_wobble_top = wwi.can_wobble_left = wwi.can_wobble_right = wwi.can_wobble_bottom = true; +} + +void WobblyWindowsEffect::wobblyCloseInit(WindowWobblyInfos& wwi, EffectWindow* w) const +{ + const QRectF& rect = w->geometry(); + QPointF center = rect.center(); + int x1 = (rect.x() + 3 * center.x()) / 4; + int x2 = (rect.x() + rect.width() + 3 * center.x()) / 4; + int y1 = (rect.y() + 3 * center.y()) / 4; + int y2 = (rect.y() + rect.height() + 3 * center.y()) / 4; + wwi.closeRect.setCoords(x1, y1, x2, y2); + + // for closing, not yet used... + for (unsigned int j = 0; j < 4; ++j) { + for (unsigned int i = 0; i < 4; ++i) { + unsigned int idx = j * 4 + i; + wwi.constraint[idx] = false; + } + } + wwi.status = Closing; +} + +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]; + +// ASSERT1(point.x >= topleft.x); +// ASSERT1(point.y >= topleft.y); +// ASSERT1(point.x <= bottomright.x); +// ASSERT1(point.y <= bottomright.y); + + qreal tx = (point.x - topleft.x) / (bottomright.x - topleft.x); + qreal ty = (point.y - topleft.y) / (bottomright.y - topleft.y); + +// ASSERT1(tx >= 0); +// ASSERT1(tx <= 1); +// ASSERT1(ty >= 0); +// ASSERT1(ty <= 1); + + // 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]; + + if (wwi.status == Closing) { + rect = wwi.closeRect; + } + + 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) { + if (wwi.status == Closing) { + w->unrefWindow(); + } + 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; +} + + +void WobblyWindowsEffect::cancelWindowGrab(KWin::EffectWindow *w, int grabRole) +{ + if (grabRole == WindowAddedGrabRole) { + if (w->data(WindowAddedGrabRole).value() != this) { + auto it = windows.find(w); + if (it != windows.end()) { + freeWobblyInfo(it.value()); + windows.erase(it); + } + } + } else if (grabRole == WindowClosedGrabRole) { + if (w->data(WindowClosedGrabRole).value() != this) { + auto it = windows.find(w); + if (it != windows.end()) { + if (it.value().status == Closing) { + w->unrefWindow(); + } + freeWobblyInfo(it.value()); + windows.erase(it); + } + } + } +} + +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..284bd52 --- /dev/null +++ b/effects/wobblywindows/wobblywindows.h @@ -0,0 +1,219 @@ +/***************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2008 Cédric Borgese + +You can Freely distribute this program under the GNU General Public +License. See the file "COPYING" for the exact licensing terms. +******************************************************************/ + +#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 moveEffectEnabled READ isMoveEffectEnabled) + Q_PROPERTY(bool openEffectEnabled READ isOpenEffectEnabled) + Q_PROPERTY(bool closeEffectEnabled READ isCloseEffectEnabled) + Q_PROPERTY(bool moveWobble READ isMoveWobble) + Q_PROPERTY(bool resizeWobble READ isResizeWobble) +public: + + WobblyWindowsEffect(); + virtual ~WobblyWindowsEffect(); + + virtual void reconfigure(ReconfigureFlags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time); + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data); + virtual void postPaintScreen(); + virtual bool isActive() const; + + 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.:wq + 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, + Openning, + Closing + }; + + 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 isMoveEffectEnabled() const { + return m_moveEffectEnabled; + } + bool isOpenEffectEnabled() const { + return m_openEffectEnabled; + } + bool isCloseEffectEnabled() const { + return m_closeEffectEnabled; + } + bool isMoveWobble() const { + return m_moveWobble; + } + bool isResizeWobble() const { + return m_resizeWobble; + } +public Q_SLOTS: + void slotWindowAdded(KWin::EffectWindow *w); + void slotWindowClosed(KWin::EffectWindow *w); + 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 cancelWindowGrab(KWin::EffectWindow *w, int grabRole); + 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 closing + QRectF closeRect; + + // 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_moveEffectEnabled; + bool m_openEffectEnabled; + bool m_closeEffectEnabled; + + bool m_moveWobble; // Expands m_moveEffectEnabled + bool m_resizeWobble; + + void initWobblyInfo(WindowWobblyInfos& wwi, QRect geometry) const; + void freeWobblyInfo(WindowWobblyInfos& wwi) const; + void wobblyOpenInit(WindowWobblyInfos& wwi) const; + void wobblyCloseInit(WindowWobblyInfos& wwi, EffectWindow* w) 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..1212fb6 --- /dev/null +++ b/effects/wobblywindows/wobblywindows.kcfg @@ -0,0 +1,66 @@ + + + + + + 0 + + + Auto + + + true + + + true + + + false + + + 15 + + + 80 + + + 10 + + + 20 + + + 20 + + + 0.0 + + + 1000.0 + + + 0.5 + + + 0.0 + + + 1000.0 + + + 5.0 + + + true + + + false + + + false + + + diff --git a/effects/wobblywindows/wobblywindows_config.cpp b/effects/wobblywindows/wobblywindows_config.cpp new file mode 100644 index 0000000..d9eaf5b --- /dev/null +++ b/effects/wobblywindows/wobblywindows_config.cpp @@ -0,0 +1,120 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008 Cédric Borgese + Copyright (C) 2008 Lucas Murray + +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, see . +*********************************************************************/ + +#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, SIGNAL(valueChanged(int)), this, SLOT(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..a1baf69 --- /dev/null +++ b/effects/wobblywindows/wobblywindows_config.desktop @@ -0,0 +1,81 @@ +[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[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]=Wobbly Windows +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[tg]=Тирезаҳои Wobbly +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..196246a --- /dev/null +++ b/effects/wobblywindows/wobblywindows_config.h @@ -0,0 +1,52 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + + Copyright (C) 2008 Cédric Borgese + Copyright (C) 2008 Lucas Murray + +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, see . +*********************************************************************/ + +#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 = 0, const QVariantList& args = QVariantList()); + ~WobblyWindowsEffectConfig(); + +public Q_SLOTS: + virtual void save(); + +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..93139a7 --- /dev/null +++ b/effects/zoom/CMakeLists.txt @@ -0,0 +1,25 @@ +####################################### +# Config +set(kwin_zoom_config_SRCS zoom_config.cpp) +ki18n_wrap_ui(kwin_zoom_config_SRCS zoom_config.ui) +qt5_add_dbus_interface(kwin_zoom_config_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) +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::Service + KF5::XmlGui +) + +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/zoom.cpp b/effects/zoom/zoom.cpp new file mode 100644 index 0000000..efdec57 --- /dev/null +++ b/effects/zoom/zoom.cpp @@ -0,0 +1,535 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2010 Sebastian Sauer + +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, see . +*********************************************************************/ + +#include "zoom.h" +// KConfigSkeleton +#include "zoomconfig.h" + +#include +#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) + , enableFocusTracking(false) + , followFocus(true) + , mousePointer(MousePointerScale) + , focusDelay(350) // in milliseconds + , imageWidth(0) + , imageHeight(0) + , isMouseHidden(false) + , xMove(0) + , yMove(0) + , moveFactor(20.0) +{ + initConfig(); + QAction* a = 0; + 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, SIGNAL(triggered(bool)), this, SLOT(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, SIGNAL(triggered(bool)), this, SLOT(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, SIGNAL(triggered(bool)), this, SLOT(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, SIGNAL(triggered(bool)), this, SLOT(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, SIGNAL(triggered(bool)), this, SLOT(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, SIGNAL(triggered(bool)), this, SLOT(moveMouseToCenter())); + + timeline.setDuration(350); + timeline.setFrameRange(0, 100); + connect(&timeline, SIGNAL(frameChanged(int)), this, SLOT(timelineFrameChanged(int))); + connect(effects, SIGNAL(mouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers)), + this, SLOT(slotMouseChanged(QPoint,QPoint,Qt::MouseButtons,Qt::MouseButtons,Qt::KeyboardModifiers,Qt::KeyboardModifiers))); + + 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(); +} + +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()); + // Enable tracking of the focused location. + bool _enableFocusTracking = ZoomConfig::enableFocusTracking(); + if (enableFocusTracking != _enableFocusTracking) { + enableFocusTracking = _enableFocusTracking; + if (QDBusConnection::sessionBus().isConnected()) { + if (enableFocusTracking) + QDBusConnection::sessionBus().connect(QStringLiteral("org.kde.kaccessibleapp"), + QStringLiteral("/Adaptor"), + QStringLiteral("org.kde.kaccessibleapp.Adaptor"), + QStringLiteral("focusChanged"), + this, SLOT(focusChanged(int,int,int,int,int,int))); + else + QDBusConnection::sessionBus().disconnect(QStringLiteral("org.kde.kaccessibleapp"), + QStringLiteral("/Adaptor"), + QStringLiteral("org.kde.kaccessibleapp.Adaptor"), + QStringLiteral("focusChanged"), + this, SLOT(focusChanged(int,int,int,int,int,int))); + } + } + // When the focus changes, move the zoom area to the focused location. + followFocus = ZoomConfig::enableFollowFocus(); + // 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, QRegion region, 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 (enableFocusTracking && followFocus) { + 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, NULL); + 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::focusChanged(int px, int py, int rx, int ry, int rwidth, int rheight) +{ + if (zoom == 1.0) + return; + const QSize screenSize = effects->virtualScreenSize(); + focusPoint = (px >= 0 && py >= 0) ? QPoint(px, py) : QPoint(rx + qMax(0, (qMin(screenSize.width(), rwidth) / 2) - 60), ry + qMax(0, (qMin(screenSize.height(), rheight) / 2) - 60)); + if (enableFocusTracking) { + 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..09bb3da --- /dev/null +++ b/effects/zoom/zoom.h @@ -0,0 +1,134 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2010 Sebastian Sauer + +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, see . +*********************************************************************/ + +#ifndef KWIN_ZOOM_H +#define KWIN_ZOOM_H + +#include +#include +#include + +namespace KWin +{ + +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 enableFocusTracking READ isEnableFocusTracking) + Q_PROPERTY(bool followFocus READ isFollowFocus) + Q_PROPERTY(int focusDelay READ configuredFocusDelay) + Q_PROPERTY(qreal moveFactor READ configuredMoveFactor) + Q_PROPERTY(qreal targetZoom READ targetZoom) +public: + ZoomEffect(); + virtual ~ZoomEffect(); + virtual void reconfigure(ReconfigureFlags flags); + virtual void prePaintScreen(ScreenPrePaintData& data, int time); + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data); + virtual void postPaintScreen(); + virtual bool isActive() const; + // for properties + qreal configuredZoomFactor() const { + return zoomFactor; + } + int configuredMousePointer() const { + return mousePointer; + } + int configuredMouseTracking() const { + return mouseTracking; + } + bool isEnableFocusTracking() const { + return enableFocusTracking; + } + bool isFollowFocus() const { + return followFocus; + } + 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 focusChanged(int px, int py, int rx, int ry, int rwidth, int rheight); + 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: + 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; + bool enableFocusTracking; + bool followFocus; + 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..c706906 --- /dev/null +++ b/effects/zoom/zoom.kcfg @@ -0,0 +1,33 @@ + + + + + + 1.2 + + + 0 + + + 0 + + + false + + + true + + + 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..9bc02e4 --- /dev/null +++ b/effects/zoom/zoom_config.cpp @@ -0,0 +1,150 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2010 Sebastian Sauer + +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, see . +*********************************************************************/ + +#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, SIGNAL(keyChange()), this, SLOT(changed())); + + // 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..03d37b1 --- /dev/null +++ b/effects/zoom/zoom_config.desktop @@ -0,0 +1,92 @@ +[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[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]=Zum +Name[is]=Aðdráttur +Name[it]=Ingrandimento +Name[ja]=ズーム +Name[kk]=Ұлғайту +Name[km]=ពង្រីក +Name[kn]=ಹಿಗ್ಗಿಸು +Name[ko]=확대/축소 +Name[ku]=Mezinkirin +Name[lt]=Iš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]=Scalare +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[tg]=Калонкунӣ +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..b59aa55 --- /dev/null +++ b/effects/zoom/zoom_config.h @@ -0,0 +1,57 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks +Copyright (C) 2010 Sebastian Sauer + +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, see . +*********************************************************************/ + +#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 = 0); +}; + +class ZoomEffectConfig : public KCModule +{ + Q_OBJECT +public: + explicit ZoomEffectConfig(QWidget* parent = 0, const QVariantList& args = QVariantList()); + virtual ~ZoomEffectConfig(); + +public Q_SLOTS: + virtual void save(); + +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..3fb45bd --- /dev/null +++ b/effects/zoom/zoom_config.ui @@ -0,0 +1,215 @@ + + + KWin::ZoomEffectConfigForm + + + + 0 + 0 + 304 + 212 + + + + + + + + + + + + + + + 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_ACCESSIBILITY=1"). + + + Enable Focus Tracking + + + + + + + false + + + When the focus changes, move the zoom area to the focused location. + + + Follow Focus + + + + + + + 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::GlobalAction + + + + + + + + KShortcutsEditor + QWidget +
KShortcutsEditor
+ 1 +
+
+ + kcfg_ZoomFactor + kcfg_MousePointer + kcfg_MouseTracking + kcfg_EnableFocusTracking + kcfg_EnableFollowFocus + + + + + kcfg_EnableFocusTracking + toggled(bool) + kcfg_EnableFollowFocus + setEnabled(bool) + + + 152 + 73 + + + 152 + 98 + + + + +
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..9f3712b --- /dev/null +++ b/egl_context_attribute_builder.cpp @@ -0,0 +1,82 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..6f4c6cb --- /dev/null +++ b/egl_context_attribute_builder.h @@ -0,0 +1,39 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..17a2606 --- /dev/null +++ b/events.cpp @@ -0,0 +1,1345 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ + +/* + + This file contains things relevant to handling incoming events. + +*/ + +#include "client.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(filter); + else + m_eventFilters.append(filter); +} + +void Workspace::unregisterEventFilter(X11EventFilter *filter) +{ + if (filter->isGenericEvent()) + m_genericEventFilters.removeOne(filter); + else + m_eventFilters.removeOne(filter); +} + + +/*! + 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); + + foreach (X11EventFilter *filter, m_genericEventFilters) { + if (filter->extension() == ge->extension && filter->genericEventTypes().contains(ge->event_type) && filter->event(e)) { + return true; + } + } + } else { + foreach (X11EventFilter *filter, m_eventFilters) { + 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 (Client* c = findClient(Predicate::WindowMatch, eventWindow)) { + if (c->windowEvent(e)) + return true; + } else if (Client* c = findClient(Predicate::WrapperIdMatch, eventWindow)) { + if (c->windowEvent(e)) + return true; + } else if (Client* c = findClient(Predicate::FrameIdMatch, eventWindow)) { + if (c->windowEvent(e)) + return true; + } else if (Client *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 (Client* 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 Client::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 == NULL) + 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 != NULL) + requestFocus(c, true); + else if (activateNextClient(NULL)) + ; // 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 Client::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(), 0, 0); + 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())); + } + } + + 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 Client::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 Client::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 Client::destroyNotifyEvent(xcb_destroy_notify_event_t *e) +{ + if (e->window != window()) + return; + destroyClient(); +} + + +/*! + Handles client messages for the client window +*/ +void Client::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 Client::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 (fullscreen_mode == 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 Client::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 Client::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->gtk_frame_extents) + detectGtkFrameExtents(); + 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 Client::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)) { + + if (options->isShadeHover()) { + cancelShadeHoverTimer(); + if (isShade()) { + shadeHoverTimer = new QTimer(this); + connect(shadeHoverTimer, SIGNAL(timeout()), this, SLOT(shadeHover())); + shadeHoverTimer->setSingleShot(true); + shadeHoverTimer->start(options->shadeHoverInterval()); + } + } +#undef MOUSE_DRIVEN_FOCUS + + enterEvent(QPoint(e->root_x, e->root_y)); + return; + } +} + +void Client::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(); + cancelShadeHoverTimer(); + if (shade_mode == ShadeHover && !isMoveResize() && !isMoveResizePointerButtonDown()) { + shadeHoverTimer = new QTimer(this); + connect(shadeHoverTimer, SIGNAL(timeout()), this, SLOT(shadeUnhover())); + shadeHoverTimer->setSingleShot(true); + shadeHoverTimer->start(options->shadeHoverInterval()); + } + 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(0); + } + return; + } +} + +#define XCapL KKeyServer::modXLock() +#define XNumL KKeyServer::modXNumLock() +#define XScrL KKeyServer::modXScrollLock() +void Client::grabButton(int modifier) +{ + 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, modifier | mods[ i ]); +} + +void Client::ungrabButton(int modifier) +{ + 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(modifier | mods[ i ]); +} +#undef XCapL +#undef XNumL +#undef XScrL + +/* + Releases the passive grab for some modifier combinations when a + window becomes active. This helps broken X programs that + missinterpret LeaveNotify events in grab mode to work properly + (Motif, AWT, Tk, ...) + */ +void Client::updateMouseGrab() +{ + if (workspace()->globalShortcutsDisabled()) { + m_wrapper.ungrabButton(); + // keep grab for the simple click without modifiers if needed (see below) + bool not_obscured = workspace()->topClientOnDesktop(VirtualDesktopManager::self()->current(), -1, true, false) == this; + if (!(!options->isClickRaise() || not_obscured)) + grabButton(XCB_NONE); + return; + } + if (isActive() && !TabBox::TabBox::self()->forcedGlobalMouseGrab()) { // see TabBox::establishTabBoxGrab() + // first grab all modifier combinations + m_wrapper.grabButton(XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC); + // remove the grab for no modifiers only if the window + // is unobscured or if the user doesn't want click raise + // (it is unobscured if it the topmost in the unconstrained stacking order, i.e. it is + // the most recently raised window) + bool not_obscured = workspace()->topClientOnDesktop(VirtualDesktopManager::self()->current(), -1, true, false) == this; + if (!options->isClickRaise() || not_obscured) + ungrabButton(XCB_NONE); + else + grabButton(XCB_NONE); + ungrabButton(XCB_MOD_MASK_SHIFT); + ungrabButton(XCB_MOD_MASK_CONTROL); + ungrabButton(XCB_MOD_MASK_CONTROL | XCB_MOD_MASK_SHIFT); + } else { + m_wrapper.ungrabButton(); + // simply grab all modifier combinations + m_wrapper.grabButton(XCB_GRAB_MODE_SYNC, XCB_GRAB_MODE_ASYNC); + } +} + +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 Client::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 - geometry().x(); + y = y_root - geometry().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 Client::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 Client::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 - geometry().x();// + padding_left; + int y = y_root - geometry().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 Client::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([](Client *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 { + workspace()->restoreFocus(); + demandAttention(); + } +} + +void Client::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 Client::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 Client::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 + Cursor::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 + Cursor::setPos(geometry().center()); + performMouseCommand(Options::MouseUnrestrictedMove, geometry().center()); + } else if (direction == NET::KeyboardSize) { + // ignore mouse coordinates given in the message, mouse position is used by the resizing algorithm + Cursor::setPos(geometry().bottomRight()); + performMouseCommand(Options::MouseUnrestrictedResize, geometry().bottomRight()); + } +} + +void Client::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(Cursor::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. + 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(geometry()); // in case shape change removes part of this window + emit geometryShapeChanged(this, geometry()); + } + 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 != geom) { + addWorkspaceRepaint(visibleRect()); // damage old area + QRect old = geom; + geom = newgeom; + emit geometryChanged(); // update shadow region + addRepaintFull(); + if (old.size() != geom.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) + getShadow(); + 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 = KWayland::Server::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..7287adf --- /dev/null +++ b/fixqopengl.h @@ -0,0 +1,37 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2018 Bhushan Shah + +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, see . +*********************************************************************/ + +#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..1054d69 --- /dev/null +++ b/focuschain.cpp @@ -0,0 +1,269 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(NULL) + , m_currentDesktop(0) +{ +} + +FocusChain::~FocusChain() +{ + s_manager = NULL; +} + +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 NULL; + } + 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 NULL; +} + +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 NULL; + } + return m_mostRecentlyUsed.first(); +} + +AbstractClient *FocusChain::nextMostRecentlyUsed(AbstractClient *reference) const +{ + if (m_mostRecentlyUsed.isEmpty()) { + return NULL; + } + 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 NULL; + } + 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 NULL; +} + +void FocusChain::makeFirstInChain(AbstractClient *client, Chain &chain) +{ + chain.removeAll(client); + if (client->isMinimized()) { // add it before the first minimized ... + for (int i = chain.count()-1; i >= 0; --i) { + if (chain.at(i)->isMinimized()) { + chain.insert(i+1, client); + return; + } + } + chain.prepend(client); // ... or at end of chain + } else { + 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..662cffd --- /dev/null +++ b/focuschain.h @@ -0,0 +1,254 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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, + MakeFirstMinimized = MakeFirst + }; + virtual ~FocusChain(); + /** + * @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 :Client* 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 :Client* 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 :Client* 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 :Client* 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 :Client* 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/geometry.cpp b/geometry.cpp new file mode 100644 index 0000000..b2f3eae --- /dev/null +++ b/geometry.cpp @@ -0,0 +1,3539 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +/* + + This file contains things relevant to geometry, i.e. workspace size, + window positions and window sizes. + +*/ + +#include "client.h" +#include "composite.h" +#include "cursor.h" +#include "netinfo.h" +#include "workspace.h" + +#include "placement.h" +#include "geometrytip.h" +#include "rules.h" +#include "screens.h" +#include "effects.h" +#include "screenedge.h" +#include +#include +#include + +#include "outline.h" +#include "shell_client.h" +#include "wayland_server.h" + +#include +#include + +namespace KWin +{ + +static inline int sign(int v) { + return (v > 0) - (v < 0); +} + +//******************************************** +// Workspace +//******************************************** + +extern int screen_number; +extern bool is_multihead; + +/*! + Resizes the workspace after an XRANDR screen size change + */ +void Workspace::desktopResized() +{ + QRect geom = screens()->geometry(); + if (rootInfo()) { + NETSize desktop_geometry; + desktop_geometry.width = geom.width(); + desktop_geometry.height = geom.height(); + rootInfo()->setDesktopGeometry(desktop_geometry); + } + + updateClientArea(); + saveOldScreenSizes(); // after updateClientArea(), so that one still uses the previous one + + // TODO: emit a signal instead and remove the deep function calls into edges and effects + ScreenEdges::self()->recreateEdges(); + + if (effects) { + static_cast(effects)->desktopResized(geom.size()); + } +} + +void Workspace::saveOldScreenSizes() +{ + olddisplaysize = screens()->displaySize(); + oldscreensizes.clear(); + for( int i = 0; + i < screens()->count(); + ++i ) + oldscreensizes.append( screens()->geometry( i )); +} + +/*! + Updates the current client areas according to the current clients. + + If the area changes or force is true, the new areas are propagated to the world. + + The client area is the area that is available for clients (that + which is not taken by windows like panels, the top-of-screen menu + etc). + + \sa clientArea() + */ + +void Workspace::updateClientArea(bool force) +{ + const Screens *s = Screens::self(); + int nscreens = s->count(); + const int numberOfDesktops = VirtualDesktopManager::self()->count(); + QVector< QRect > new_wareas(numberOfDesktops + 1); + QVector< StrutRects > new_rmoveareas(numberOfDesktops + 1); + QVector< QVector< QRect > > new_sareas(numberOfDesktops + 1); + QVector< QRect > screens(nscreens); + QRect desktopArea; + for (int i = 0; i < nscreens; i++) { + desktopArea |= s->geometry(i); + } + for (int iS = 0; + iS < nscreens; + iS ++) { + screens [iS] = s->geometry(iS); + } + for (int i = 1; + i <= numberOfDesktops; + ++i) { + new_wareas[ i ] = desktopArea; + new_sareas[ i ].resize(nscreens); + for (int iS = 0; + iS < nscreens; + iS ++) + new_sareas[ i ][ iS ] = screens[ iS ]; + } + for (ClientList::ConstIterator it = clients.constBegin(); it != clients.constEnd(); ++it) { + if (!(*it)->hasStrut()) + continue; + QRect r = (*it)->adjustedClientArea(desktopArea, desktopArea); + // sanity check that a strut doesn't exclude a complete screen geometry + // this is a violation to EWMH, as KWin just ignores the strut + for (int i = 0; i < Screens::self()->count(); i++) { + if (!r.intersects(Screens::self()->geometry(i))) { + qCDebug(KWIN_CORE) << "Adjusted client area would exclude a complete screen, ignore"; + r = desktopArea; + break; + } + } + StrutRects strutRegion = (*it)->strutRects(); + const QRect clientsScreenRect = KWin::screens()->geometry((*it)->screen()); + for (auto strut = strutRegion.begin(); strut != strutRegion.end(); strut++) { + *strut = StrutRect((*strut).intersected(clientsScreenRect), (*strut).area()); + } + + // Ignore offscreen xinerama struts. These interfere with the larger monitors on the setup + // and should be ignored so that applications that use the work area to work out where + // windows can go can use the entire visible area of the larger monitors. + // This goes against the EWMH description of the work area but it is a toss up between + // having unusable sections of the screen (Which can be quite large with newer monitors) + // or having some content appear offscreen (Relatively rare compared to other). + bool hasOffscreenXineramaStrut = (*it)->hasOffscreenXineramaStrut(); + + if ((*it)->isOnAllDesktops()) { + for (int i = 1; + i <= numberOfDesktops; + ++i) { + if (!hasOffscreenXineramaStrut) + new_wareas[ i ] = new_wareas[ i ].intersected(r); + new_rmoveareas[ i ] += strutRegion; + for (int iS = 0; + iS < nscreens; + iS ++) { + const auto geo = new_sareas[ i ][ iS ].intersected( + (*it)->adjustedClientArea(desktopArea, screens[ iS ])); + // ignore the geometry if it results in the screen getting removed completely + if (!geo.isEmpty()) { + new_sareas[ i ][ iS ] = geo; + } + } + } + } else { + if (!hasOffscreenXineramaStrut) + new_wareas[(*it)->desktop()] = new_wareas[(*it)->desktop()].intersected(r); + new_rmoveareas[(*it)->desktop()] += strutRegion; + for (int iS = 0; + iS < nscreens; + iS ++) { +// qDebug() << "adjusting new_sarea: " << screens[ iS ]; + const auto geo = new_sareas[(*it)->desktop()][ iS ].intersected( + (*it)->adjustedClientArea(desktopArea, screens[ iS ])); + // ignore the geometry if it results in the screen getting removed completely + if (!geo.isEmpty()) { + new_sareas[(*it)->desktop()][ iS ] = geo; + } + } + } + } + if (waylandServer()) { + auto updateStrutsForWaylandClient = [&] (ShellClient *c) { + // assuming that only docks have "struts" and that all docks have a strut + if (!c->hasStrut()) { + return; + } + auto margins = [c] (const QRect &geometry) { + QMargins margins; + if (!geometry.intersects(c->geometry())) { + return margins; + } + // figure out which areas of the overall screen setup it borders + const bool left = c->geometry().left() == geometry.left(); + const bool right = c->geometry().right() == geometry.right(); + const bool top = c->geometry().top() == geometry.top(); + const bool bottom = c->geometry().bottom() == geometry.bottom(); + const bool horizontal = c->geometry().width() >= c->geometry().height(); + if (left && ((!top && !bottom) || !horizontal)) { + margins.setLeft(c->geometry().width()); + } + if (right && ((!top && !bottom) || !horizontal)) { + margins.setRight(c->geometry().width()); + } + if (top && ((!left && !right) || horizontal)) { + margins.setTop(c->geometry().height()); + } + if (bottom && ((!left && !right) || horizontal)) { + margins.setBottom(c->geometry().height()); + } + return margins; + }; + auto marginsToStrutArea = [] (const QMargins &margins) { + if (margins.left() != 0) { + return StrutAreaLeft; + } + if (margins.right() != 0) { + return StrutAreaRight; + } + if (margins.top() != 0) { + return StrutAreaTop; + } + if (margins.bottom() != 0) { + return StrutAreaBottom; + } + return StrutAreaInvalid; + }; + const auto strut = margins(KWin::screens()->geometry(c->screen())); + const StrutRects strutRegion = StrutRects{StrutRect(c->geometry(), marginsToStrutArea(strut))}; + QRect r = desktopArea - margins(KWin::screens()->geometry()); + if (c->isOnAllDesktops()) { + for (int i = 1; i <= numberOfDesktops; ++i) { + new_wareas[ i ] = new_wareas[ i ].intersected(r); + for (int iS = 0; iS < nscreens; ++iS) { + new_sareas[ i ][ iS ] = new_sareas[ i ][ iS ].intersected(screens[iS] - margins(screens[iS])); + } + new_rmoveareas[ i ] += strutRegion; + } + } else { + new_wareas[c->desktop()] = new_wareas[c->desktop()].intersected(r); + for (int iS = 0; iS < nscreens; iS++) { + new_sareas[c->desktop()][ iS ] = new_sareas[c->desktop()][ iS ].intersected(screens[iS] - margins(screens[iS])); + } + new_rmoveareas[ c->desktop() ] += strutRegion; + } + }; + const auto clients = waylandServer()->clients(); + for (auto c : clients) { + updateStrutsForWaylandClient(c); + } + const auto internalClients = waylandServer()->internalClients(); + for (auto c : internalClients) { + updateStrutsForWaylandClient(c); + } + } +#if 0 + for (int i = 1; + i <= numberOfDesktops(); + ++i) { + for (int iS = 0; + iS < nscreens; + iS ++) + qCDebug(KWIN_CORE) << "new_sarea: " << new_sareas[ i ][ iS ]; + } +#endif + + bool changed = force; + + if (screenarea.isEmpty()) + changed = true; + + for (int i = 1; + !changed && i <= numberOfDesktops; + ++i) { + if (workarea[ i ] != new_wareas[ i ]) + changed = true; + if (restrictedmovearea[ i ] != new_rmoveareas[ i ]) + changed = true; + if (screenarea[ i ].size() != new_sareas[ i ].size()) + changed = true; + for (int iS = 0; + !changed && iS < nscreens; + iS ++) + if (new_sareas[ i ][ iS ] != screenarea [ i ][ iS ]) + changed = true; + } + + if (changed) { + workarea = new_wareas; + oldrestrictedmovearea = restrictedmovearea; + restrictedmovearea = new_rmoveareas; + screenarea = new_sareas; + if (rootInfo()) { + NETRect r; + for (int i = 1; i <= numberOfDesktops; i++) { + r.pos.x = workarea[ i ].x(); + r.pos.y = workarea[ i ].y(); + r.size.width = workarea[ i ].width(); + r.size.height = workarea[ i ].height(); + rootInfo()->setWorkArea(i, r); + } + } + + for (auto it = m_allClients.constBegin(); + it != m_allClients.constEnd(); + ++it) + (*it)->checkWorkspacePosition(); + for (ClientList::ConstIterator it = desktops.constBegin(); + it != desktops.constEnd(); + ++it) + (*it)->checkWorkspacePosition(); + + oldrestrictedmovearea.clear(); // reset, no longer valid or needed + } +} + +void Workspace::updateClientArea() +{ + updateClientArea(false); +} + + +/*! + returns the area available for clients. This is the desktop + geometry minus windows on the dock. Placement algorithms should + refer to this rather than geometry(). + + \sa geometry() + */ + +QRect Workspace::clientArea(clientAreaOption opt, int screen, int desktop) const +{ + if (desktop == NETWinInfo::OnAllDesktops || desktop == 0) + desktop = VirtualDesktopManager::self()->current(); + if (screen == -1) + screen = screens()->current(); + const QSize displaySize = screens()->displaySize(); + + QRect sarea, warea; + + if (is_multihead) { + sarea = (!screenarea.isEmpty() + && screen < screenarea[ desktop ].size()) // screens may be missing during KWin initialization or screen config changes + ? screenarea[ desktop ][ screen_number ] + : screens()->geometry(screen_number); + warea = workarea[ desktop ].isNull() + ? screens()->geometry(screen_number) + : workarea[ desktop ]; + } else { + sarea = (!screenarea.isEmpty() + && screen < screenarea[ desktop ].size()) // screens may be missing during KWin initialization or screen config changes + ? screenarea[ desktop ][ screen ] + : screens()->geometry(screen); + warea = workarea[ desktop ].isNull() + ? QRect(0, 0, displaySize.width(), displaySize.height()) + : workarea[ desktop ]; + } + + switch(opt) { + case MaximizeArea: + case PlacementArea: + return sarea; + case MaximizeFullArea: + case FullScreenArea: + case MovementArea: + case ScreenArea: + if (is_multihead) + return screens()->geometry(screen_number); + else + return screens()->geometry(screen); + case WorkArea: + if (is_multihead) + return sarea; + else + return warea; + case FullArea: + return QRect(0, 0, displaySize.width(), displaySize.height()); + } + abort(); +} + + +QRect Workspace::clientArea(clientAreaOption opt, const QPoint& p, int desktop) const +{ + return clientArea(opt, screens()->number(p), desktop); +} + +QRect Workspace::clientArea(clientAreaOption opt, const AbstractClient* c) const +{ + return clientArea(opt, c->geometry().center(), c->desktop()); +} + +QRegion Workspace::restrictedMoveArea(int desktop, StrutAreas areas) const +{ + if (desktop == NETWinInfo::OnAllDesktops || desktop == 0) + desktop = VirtualDesktopManager::self()->current(); + QRegion region; + foreach (const StrutRect & rect, restrictedmovearea[desktop]) + if (areas & rect.area()) + region += rect; + return region; +} + +bool Workspace::inUpdateClientArea() const +{ + return !oldrestrictedmovearea.isEmpty(); +} + +QRegion Workspace::previousRestrictedMoveArea(int desktop, StrutAreas areas) const +{ + if (desktop == NETWinInfo::OnAllDesktops || desktop == 0) + desktop = VirtualDesktopManager::self()->current(); + QRegion region; + foreach (const StrutRect & rect, oldrestrictedmovearea.at(desktop)) + if (areas & rect.area()) + region += rect; + return region; +} + +QVector< QRect > Workspace::previousScreenSizes() const +{ + return oldscreensizes; +} + +int Workspace::oldDisplayWidth() const +{ + return olddisplaysize.width(); +} + +int Workspace::oldDisplayHeight() const +{ + return olddisplaysize.height(); +} + +/*! + Client \a c is moved around to position \a pos. This gives the + workspace the opportunity to interveniate and to implement + snap-to-windows functionality. + + The parameter \a snapAdjust is a multiplier used to calculate the + effective snap zones. When 1.0, it means that the snap zones will be + used without change. + */ +QPoint Workspace::adjustClientPosition(AbstractClient* c, QPoint pos, bool unrestricted, double snapAdjust) +{ + QSize borderSnapZone(options->borderSnapZone(), options->borderSnapZone()); + QRect maxRect; + int guideMaximized = MaximizeRestore; + if (c->maximizeMode() != MaximizeRestore) { + maxRect = clientArea(MaximizeArea, pos + c->rect().center(), c->desktop()); + QRect geo = c->geometry(); + if (c->maximizeMode() & MaximizeHorizontal && (geo.x() == maxRect.left() || geo.right() == maxRect.right())) { + guideMaximized |= MaximizeHorizontal; + borderSnapZone.setWidth(qMax(borderSnapZone.width() + 2, maxRect.width() / 16)); + } + if (c->maximizeMode() & MaximizeVertical && (geo.y() == maxRect.top() || geo.bottom() == maxRect.bottom())) { + guideMaximized |= MaximizeVertical; + borderSnapZone.setHeight(qMax(borderSnapZone.height() + 2, maxRect.height() / 16)); + } + } + + if (options->windowSnapZone() || !borderSnapZone.isNull() || options->centerSnapZone()) { + + const bool sOWO = options->isSnapOnlyWhenOverlapping(); + const int screen = screens()->number(pos + c->rect().center()); + if (maxRect.isNull()) + maxRect = clientArea(MovementArea, screen, c->desktop()); + const int xmin = maxRect.left(); + const int xmax = maxRect.right() + 1; //desk size + const int ymin = maxRect.top(); + const int ymax = maxRect.bottom() + 1; + + const int cx(pos.x()); + const int cy(pos.y()); + const int cw(c->width()); + const int ch(c->height()); + const int rx(cx + cw); + const int ry(cy + ch); //these don't change + + int nx(cx), ny(cy); //buffers + int deltaX(xmax); + int deltaY(ymax); //minimum distance to other clients + + int lx, ly, lrx, lry; //coords and size for the comparison client, l + + // border snap + const int snapX = borderSnapZone.width() * snapAdjust; //snap trigger + const int snapY = borderSnapZone.height() * snapAdjust; + if (snapX || snapY) { + QRect geo = c->geometry(); + const QPoint cp = c->clientPos(); + const QSize cs = geo.size() - c->clientSize(); + int padding[4] = { cp.x(), cs.width() - cp.x(), cp.y(), cs.height() - cp.y() }; + + // snap to titlebar / snap to window borders on inner screen edges + Client::Position titlePos = c->titlebarPosition(); + if (padding[0] && (titlePos == Client::PositionLeft || (c->maximizeMode() & MaximizeHorizontal) || + screens()->intersecting(geo.translated(maxRect.x() - (padding[0] + geo.x()), 0)) > 1)) + padding[0] = 0; + if (padding[1] && (titlePos == Client::PositionRight || (c->maximizeMode() & MaximizeHorizontal) || + screens()->intersecting(geo.translated(maxRect.right() + padding[1] - geo.right(), 0)) > 1)) + padding[1] = 0; + if (padding[2] && (titlePos == Client::PositionTop || (c->maximizeMode() & MaximizeVertical) || + screens()->intersecting(geo.translated(0, maxRect.y() - (padding[2] + geo.y()))) > 1)) + padding[2] = 0; + if (padding[3] && (titlePos == Client::PositionBottom || (c->maximizeMode() & MaximizeVertical) || + screens()->intersecting(geo.translated(0, maxRect.bottom() + padding[3] - geo.bottom())) > 1)) + padding[3] = 0; + if ((sOWO ? (cx < xmin) : true) && (qAbs(xmin - cx) < snapX)) { + deltaX = xmin - cx; + nx = xmin - padding[0]; + } + if ((sOWO ? (rx > xmax) : true) && (qAbs(rx - xmax) < snapX) && (qAbs(xmax - rx) < deltaX)) { + deltaX = rx - xmax; + nx = xmax - cw + padding[1]; + } + + if ((sOWO ? (cy < ymin) : true) && (qAbs(ymin - cy) < snapY)) { + deltaY = ymin - cy; + ny = ymin - padding[2]; + } + if ((sOWO ? (ry > ymax) : true) && (qAbs(ry - ymax) < snapY) && (qAbs(ymax - ry) < deltaY)) { + deltaY = ry - ymax; + ny = ymax - ch + padding[3]; + } + } + + // windows snap + int snap = options->windowSnapZone() * snapAdjust; + if (snap) { + for (auto l = m_allClients.constBegin(); l != m_allClients.constEnd(); ++l) { + if ((*l) == c) + continue; + if ((*l)->isMinimized()) + continue; // is minimized + if (!(*l)->isShown(false)) + continue; + if ((*l)->tabGroup() && (*l) != (*l)->tabGroup()->current()) + continue; // is not active tab + if (!((*l)->isOnDesktop(c->desktop()) || c->isOnDesktop((*l)->desktop()))) + continue; // wrong virtual desktop + if (!(*l)->isOnCurrentActivity()) + continue; // wrong activity + if ((*l)->isDesktop() || (*l)->isSplash()) + continue; + + lx = (*l)->x(); + ly = (*l)->y(); + lrx = lx + (*l)->width(); + lry = ly + (*l)->height(); + + if (!(guideMaximized & MaximizeHorizontal) && + (((cy <= lry) && (cy >= ly)) || ((ry >= ly) && (ry <= lry)) || ((cy <= ly) && (ry >= lry)))) { + if ((sOWO ? (cx < lrx) : true) && (qAbs(lrx - cx) < snap) && (qAbs(lrx - cx) < deltaX)) { + deltaX = qAbs(lrx - cx); + nx = lrx; + } + if ((sOWO ? (rx > lx) : true) && (qAbs(rx - lx) < snap) && (qAbs(rx - lx) < deltaX)) { + deltaX = qAbs(rx - lx); + nx = lx - cw; + } + } + + if (!(guideMaximized & MaximizeVertical) && + (((cx <= lrx) && (cx >= lx)) || ((rx >= lx) && (rx <= lrx)) || ((cx <= lx) && (rx >= lrx)))) { + if ((sOWO ? (cy < lry) : true) && (qAbs(lry - cy) < snap) && (qAbs(lry - cy) < deltaY)) { + deltaY = qAbs(lry - cy); + ny = lry; + } + //if ( (qAbs( ry-ly ) < snap) && (qAbs( ry - ly ) < deltaY )) + if ((sOWO ? (ry > ly) : true) && (qAbs(ry - ly) < snap) && (qAbs(ry - ly) < deltaY)) { + deltaY = qAbs(ry - ly); + ny = ly - ch; + } + } + + // Corner snapping + if (!(guideMaximized & MaximizeVertical) && (nx == lrx || nx + cw == lx)) { + if ((sOWO ? (ry > lry) : true) && (qAbs(lry - ry) < snap) && (qAbs(lry - ry) < deltaY)) { + deltaY = qAbs(lry - ry); + ny = lry - ch; + } + if ((sOWO ? (cy < ly) : true) && (qAbs(cy - ly) < snap) && (qAbs(cy - ly) < deltaY)) { + deltaY = qAbs(cy - ly); + ny = ly; + } + } + if (!(guideMaximized & MaximizeHorizontal) && (ny == lry || ny + ch == ly)) { + if ((sOWO ? (rx > lrx) : true) && (qAbs(lrx - rx) < snap) && (qAbs(lrx - rx) < deltaX)) { + deltaX = qAbs(lrx - rx); + nx = lrx - cw; + } + if ((sOWO ? (cx < lx) : true) && (qAbs(cx - lx) < snap) && (qAbs(cx - lx) < deltaX)) { + deltaX = qAbs(cx - lx); + nx = lx; + } + } + } + } + + // center snap + snap = options->centerSnapZone() * snapAdjust; //snap trigger + if (snap) { + int diffX = qAbs((xmin + xmax) / 2 - (cx + cw / 2)); + int diffY = qAbs((ymin + ymax) / 2 - (cy + ch / 2)); + if (diffX < snap && diffY < snap && diffX < deltaX && diffY < deltaY) { + // Snap to center of screen + nx = (xmin + xmax) / 2 - cw / 2; + ny = (ymin + ymax) / 2 - ch / 2; + } else if (options->borderSnapZone()) { + // Enhance border snap + if ((nx == xmin || nx == xmax - cw) && diffY < snap && diffY < deltaY) { + // Snap to vertical center on screen edge + ny = (ymin + ymax) / 2 - ch / 2; + } else if (((unrestricted ? ny == ymin : ny <= ymin) || ny == ymax - ch) && + diffX < snap && diffX < deltaX) { + // Snap to horizontal center on screen edge + nx = (xmin + xmax) / 2 - cw / 2; + } + } + } + + pos = QPoint(nx, ny); + } + return pos; +} + +QRect Workspace::adjustClientSize(AbstractClient* c, QRect moveResizeGeom, int mode) +{ + //adapted from adjustClientPosition on 29May2004 + //this function is called when resizing a window and will modify + //the new dimensions to snap to other windows/borders if appropriate + if (options->windowSnapZone() || options->borderSnapZone()) { // || options->centerSnapZone ) + const bool sOWO = options->isSnapOnlyWhenOverlapping(); + + const QRect maxRect = clientArea(MovementArea, c->rect().center(), c->desktop()); + const int xmin = maxRect.left(); + const int xmax = maxRect.right(); //desk size + const int ymin = maxRect.top(); + const int ymax = maxRect.bottom(); + + const int cx(moveResizeGeom.left()); + const int cy(moveResizeGeom.top()); + const int rx(moveResizeGeom.right()); + const int ry(moveResizeGeom.bottom()); + + int newcx(cx), newcy(cy); //buffers + int newrx(rx), newry(ry); + int deltaX(xmax); + int deltaY(ymax); //minimum distance to other clients + + int lx, ly, lrx, lry; //coords and size for the comparison client, l + + // border snap + int snap = options->borderSnapZone(); //snap trigger + if (snap) { + deltaX = int(snap); + deltaY = int(snap); + +#define SNAP_BORDER_TOP \ + if ((sOWO?(newcyymax):true) && (qAbs(ymax-newry)xmax):true) && (qAbs(xmax-newrx)windowSnapZone(); + if (snap) { + deltaX = int(snap); + deltaY = int(snap); + for (auto l = m_allClients.constBegin(); l != m_allClients.constEnd(); ++l) { + if ((*l)->isOnDesktop(VirtualDesktopManager::self()->current()) && + !(*l)->isMinimized() + && (*l) != c) { + lx = (*l)->x() - 1; + ly = (*l)->y() - 1; + lrx = (*l)->x() + (*l)->width(); + lry = (*l)->y() + (*l)->height(); + +#define WITHIN_HEIGHT ((( newcy <= lry ) && ( newcy >= ly )) || \ + (( newry >= ly ) && ( newry <= lry )) || \ + (( newcy <= ly ) && ( newry >= lry )) ) + +#define WITHIN_WIDTH ( (( cx <= lrx ) && ( cx >= lx )) || \ + (( rx >= lx ) && ( rx <= lrx )) || \ + (( cx <= lx ) && ( rx >= lrx )) ) + +#define SNAP_WINDOW_TOP if ( (sOWO?(newcyly):true) \ + && WITHIN_WIDTH \ + && (qAbs( ly - newry ) < deltaY) ) { \ + deltaY = qAbs( ly - newry ); \ + newry=ly; \ +} + +#define SNAP_WINDOW_LEFT if ( (sOWO?(newcxlx):true) \ + && WITHIN_HEIGHT \ + && (qAbs( lx - newrx ) < deltaX)) \ +{ \ + deltaX = qAbs( lx - newrx ); \ + newrx=lx; \ +} + +#define SNAP_WINDOW_C_TOP if ( (sOWO?(newcylry):true) \ + && (newcx == lrx || newrx == lx) \ + && qAbs(lry-newry) < deltaY ) { \ + deltaY = qAbs( lry - newry - 1 ); \ + newry = lry - 1; \ +} + +#define SNAP_WINDOW_C_LEFT if ( (sOWO?(newcxlrx):true) \ + && (newcy == lry || newry == ly) \ + && qAbs(lrx-newrx) < deltaX ) { \ + deltaX = qAbs( lrx - newrx - 1 ); \ + newrx = lrx - 1; \ +} + + switch(mode) { + case Client::PositionBottomRight: + SNAP_WINDOW_BOTTOM + SNAP_WINDOW_RIGHT + SNAP_WINDOW_C_BOTTOM + SNAP_WINDOW_C_RIGHT + break; + case Client::PositionRight: + SNAP_WINDOW_RIGHT + SNAP_WINDOW_C_RIGHT + break; + case Client::PositionBottom: + SNAP_WINDOW_BOTTOM + SNAP_WINDOW_C_BOTTOM + break; + case Client::PositionTopLeft: + SNAP_WINDOW_TOP + SNAP_WINDOW_LEFT + SNAP_WINDOW_C_TOP + SNAP_WINDOW_C_LEFT + break; + case Client::PositionLeft: + SNAP_WINDOW_LEFT + SNAP_WINDOW_C_LEFT + break; + case Client::PositionTop: + SNAP_WINDOW_TOP + SNAP_WINDOW_C_TOP + break; + case Client::PositionTopRight: + SNAP_WINDOW_TOP + SNAP_WINDOW_RIGHT + SNAP_WINDOW_C_TOP + SNAP_WINDOW_C_RIGHT + break; + case Client::PositionBottomLeft: + SNAP_WINDOW_BOTTOM + SNAP_WINDOW_LEFT + SNAP_WINDOW_C_BOTTOM + SNAP_WINDOW_C_LEFT + break; + default: + abort(); + break; + } + } + } + } + + // center snap + //snap = options->centerSnapZone; + //if (snap) + // { + // // Don't resize snap to center as it interferes too much + // // There are two ways of implementing this if wanted: + // // 1) Snap only to the same points that the move snap does, and + // // 2) Snap to the horizontal and vertical center lines of the screen + // } + + moveResizeGeom = QRect(QPoint(newcx, newcy), QPoint(newrx, newry)); + } + return moveResizeGeom; +} + +/*! + Marks the client as being moved around by the user. + */ +void Workspace::setClientIsMoving(AbstractClient *c) +{ + Q_ASSERT(!c || !movingClient); // Catch attempts to move a second + // window while still moving the first one. + movingClient = c; + if (movingClient) + ++block_focus; + else + --block_focus; +} + +// When kwin crashes, windows will not be gravitated back to their original position +// and will remain offset by the size of the decoration. So when restarting, fix this +// (the property with the size of the frame remains on the window after the crash). +void Workspace::fixPositionAfterCrash(xcb_window_t w, const xcb_get_geometry_reply_t *geometry) +{ + NETWinInfo i(connection(), w, rootWindow(), NET::WMFrameExtents, 0); + NETStrut frame = i.frameExtents(); + + if (frame.left != 0 || frame.top != 0) { + // left and top needed due to narrowing conversations restrictions in C++11 + const uint32_t left = frame.left; + const uint32_t top = frame.top; + const uint32_t values[] = { geometry->x - left, geometry->y - top }; + xcb_configure_window(connection(), w, XCB_CONFIG_WINDOW_X | XCB_CONFIG_WINDOW_Y, values); + } +} + +//******************************************** +// Client +//******************************************** + +/*! + Returns \a area with the client's strut taken into account. + + Used from Workspace in updateClientArea. + */ +// TODO move to Workspace? + +QRect Client::adjustedClientArea(const QRect &desktopArea, const QRect& area) const +{ + QRect r = area; + NETExtendedStrut str = strut(); + QRect stareaL = QRect( + 0, + str . left_start, + str . left_width, + str . left_end - str . left_start + 1); + QRect stareaR = QRect( + desktopArea . right() - str . right_width + 1, + str . right_start, + str . right_width, + str . right_end - str . right_start + 1); + QRect stareaT = QRect( + str . top_start, + 0, + str . top_end - str . top_start + 1, + str . top_width); + QRect stareaB = QRect( + str . bottom_start, + desktopArea . bottom() - str . bottom_width + 1, + str . bottom_end - str . bottom_start + 1, + str . bottom_width); + + QRect screenarea = workspace()->clientArea(ScreenArea, this); + // HACK: workarea handling is not xinerama aware, so if this strut + // reserves place at a xinerama edge that's inside the virtual screen, + // ignore the strut for workspace setting. + if (area == QRect(QPoint(0, 0), screens()->displaySize())) { + if (stareaL.left() < screenarea.left()) + stareaL = QRect(); + if (stareaR.right() > screenarea.right()) + stareaR = QRect(); + if (stareaT.top() < screenarea.top()) + stareaT = QRect(); + if (stareaB.bottom() < screenarea.bottom()) + stareaB = QRect(); + } + // Handle struts at xinerama edges that are inside the virtual screen. + // They're given in virtual screen coordinates, make them affect only + // their xinerama screen. + stareaL.setLeft(qMax(stareaL.left(), screenarea.left())); + stareaR.setRight(qMin(stareaR.right(), screenarea.right())); + stareaT.setTop(qMax(stareaT.top(), screenarea.top())); + stareaB.setBottom(qMin(stareaB.bottom(), screenarea.bottom())); + + if (stareaL . intersects(area)) { +// qDebug() << "Moving left of: " << r << " to " << stareaL.right() + 1; + r . setLeft(stareaL . right() + 1); + } + if (stareaR . intersects(area)) { +// qDebug() << "Moving right of: " << r << " to " << stareaR.left() - 1; + r . setRight(stareaR . left() - 1); + } + if (stareaT . intersects(area)) { +// qDebug() << "Moving top of: " << r << " to " << stareaT.bottom() + 1; + r . setTop(stareaT . bottom() + 1); + } + if (stareaB . intersects(area)) { +// qDebug() << "Moving bottom of: " << r << " to " << stareaB.top() - 1; + r . setBottom(stareaB . top() - 1); + } + + return r; +} + +NETExtendedStrut Client::strut() const +{ + NETExtendedStrut ext = info->extendedStrut(); + NETStrut str = info->strut(); + const QSize displaySize = screens()->displaySize(); + if (ext.left_width == 0 && ext.right_width == 0 && ext.top_width == 0 && ext.bottom_width == 0 + && (str.left != 0 || str.right != 0 || str.top != 0 || str.bottom != 0)) { + // build extended from simple + if (str.left != 0) { + ext.left_width = str.left; + ext.left_start = 0; + ext.left_end = displaySize.height(); + } + if (str.right != 0) { + ext.right_width = str.right; + ext.right_start = 0; + ext.right_end = displaySize.height(); + } + if (str.top != 0) { + ext.top_width = str.top; + ext.top_start = 0; + ext.top_end = displaySize.width(); + } + if (str.bottom != 0) { + ext.bottom_width = str.bottom; + ext.bottom_start = 0; + ext.bottom_end = displaySize.width(); + } + } + return ext; +} + +StrutRect Client::strutRect(StrutArea area) const +{ + assert(area != StrutAreaAll); // Not valid + const QSize displaySize = screens()->displaySize(); + NETExtendedStrut strutArea = strut(); + switch(area) { + case StrutAreaTop: + if (strutArea.top_width != 0) + return StrutRect(QRect( + strutArea.top_start, 0, + strutArea.top_end - strutArea.top_start, strutArea.top_width + ), StrutAreaTop); + break; + case StrutAreaRight: + if (strutArea.right_width != 0) + return StrutRect(QRect( + displaySize.width() - strutArea.right_width, strutArea.right_start, + strutArea.right_width, strutArea.right_end - strutArea.right_start + ), StrutAreaRight); + break; + case StrutAreaBottom: + if (strutArea.bottom_width != 0) + return StrutRect(QRect( + strutArea.bottom_start, displaySize.height() - strutArea.bottom_width, + strutArea.bottom_end - strutArea.bottom_start, strutArea.bottom_width + ), StrutAreaBottom); + break; + case StrutAreaLeft: + if (strutArea.left_width != 0) + return StrutRect(QRect( + 0, strutArea.left_start, + strutArea.left_width, strutArea.left_end - strutArea.left_start + ), StrutAreaLeft); + break; + default: + abort(); // Not valid + } + return StrutRect(); // Null rect +} + +StrutRects Client::strutRects() const +{ + StrutRects region; + region += strutRect(StrutAreaTop); + region += strutRect(StrutAreaRight); + region += strutRect(StrutAreaBottom); + region += strutRect(StrutAreaLeft); + return region; +} + +bool Client::hasStrut() const +{ + NETExtendedStrut ext = strut(); + if (ext.left_width == 0 && ext.right_width == 0 && ext.top_width == 0 && ext.bottom_width == 0) + return false; + return true; +} + +bool Client::hasOffscreenXineramaStrut() const +{ + // Get strut as a QRegion + QRegion region; + region += strutRect(StrutAreaTop); + region += strutRect(StrutAreaRight); + region += strutRect(StrutAreaBottom); + region += strutRect(StrutAreaLeft); + + // Remove all visible areas so that only the invisible remain + for (int i = 0; i < screens()->count(); i ++) + region -= screens()->geometry(i); + + // If there's anything left then we have an offscreen strut + return !region.isEmpty(); +} + +void AbstractClient::checkWorkspacePosition(QRect oldGeometry, int oldDesktop, QRect oldClientGeometry) +{ + enum { Left = 0, Top, Right, Bottom }; + const int border[4] = { borderLeft(), borderTop(), borderRight(), borderBottom() }; + if( !oldGeometry.isValid()) + oldGeometry = geometry(); + if( oldDesktop == -2 ) + oldDesktop = desktop(); + if (!oldClientGeometry.isValid()) + oldClientGeometry = oldGeometry.adjusted(border[Left], border[Top], -border[Right], -border[Bottom]); + if (isDesktop()) + return; + if (isFullScreen()) { + QRect area = workspace()->clientArea(FullScreenArea, this); + if (geometry() != area) + setGeometry(area); + return; + } + if (isDock()) + return; + + if (maximizeMode() != MaximizeRestore) { + // TODO update geom_restore? + changeMaximize(false, false, true); // adjust size + const QRect screenArea = workspace()->clientArea(ScreenArea, this); + QRect geom = geometry(); + checkOffscreenPosition(&geom, screenArea); + setGeometry(geom); + return; + } + + if (quickTileMode() != QuickTileMode(QuickTileFlag::None)) { + setGeometry(electricBorderMaximizeGeometry(geometry().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; + QRect oldGeomTall; + QRect oldGeomWide; + const auto displaySize = screens()->displaySize(); + if( workspace()->inUpdateClientArea()) { + // we need to find the screen area as it was before the change + oldScreenArea = QRect( 0, 0, workspace()->oldDisplayWidth(), workspace()->oldDisplayHeight()); + oldGeomTall = QRect(oldGeometry.x(), 0, oldGeometry.width(), workspace()->oldDisplayHeight()); // Full screen height + oldGeomWide = QRect(0, oldGeometry.y(), workspace()->oldDisplayWidth(), oldGeometry.height()); // Full screen width + 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); + oldGeomTall = QRect(oldGeometry.x(), 0, oldGeometry.width(), displaySize.height()); // Full screen height + oldGeomWide = QRect(0, oldGeometry.y(), displaySize.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(), 0, newGeom.width(), displaySize.height()); // Full screen height + const QRect newGeomWide = QRect(0, newGeom.y(), displaySize.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 + + foreach (const QRect & r, (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaTop).rects()) { + QRect rect = r & oldGeomTall; + if (!rect.isEmpty()) + oldTopMax = qMax(oldTopMax, rect.y() + rect.height()); + } + foreach (const QRect & r, (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaRight).rects()) { + QRect rect = r & oldGeomWide; + if (!rect.isEmpty()) + oldRightMax = qMin(oldRightMax, rect.x()); + } + foreach (const QRect & r, (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaBottom).rects()) { + QRect rect = r & oldGeomTall; + if (!rect.isEmpty()) + oldBottomMax = qMin(oldBottomMax, rect.y()); + } + foreach (const QRect & r, (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaLeft).rects()) { + QRect rect = r & oldGeomWide; + if (!rect.isEmpty()) + oldLeftMax = qMax(oldLeftMax, rect.x() + rect.width()); + } + + // These 4 compute new bounds + foreach (const QRect & r, workspace()->restrictedMoveArea(desktop(), StrutAreaTop).rects()) { + QRect rect = r & newGeomTall; + if (!rect.isEmpty()) + topMax = qMax(topMax, rect.y() + rect.height()); + } + foreach (const QRect & r, workspace()->restrictedMoveArea(desktop(), StrutAreaRight).rects()) { + QRect rect = r & newGeomWide; + if (!rect.isEmpty()) + rightMax = qMin(rightMax, rect.x()); + } + foreach (const QRect & r, workspace()->restrictedMoveArea(desktop(), StrutAreaBottom).rects()) { + QRect rect = r & newGeomTall; + if (!rect.isEmpty()) + bottomMax = qMin(bottomMax, rect.y()); + } + foreach (const QRect & r, workspace()->restrictedMoveArea(desktop(), StrutAreaLeft).rects()) { + 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(adjustedSize(newGeom.size())); + + if (newGeom != geometry()) + setGeometry(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); + } +} + +/*! + Adjust the frame size \a frame according to he window's size hints. + */ +QSize AbstractClient::adjustedSize(const QSize& frame, Sizemode mode) const +{ + // first, get the window size for the given frame size s + QSize wsize(frame.width() - (borderLeft() + borderRight()), + frame.height() - (borderTop() + borderBottom())); + if (wsize.isEmpty()) + wsize = QSize(qMax(wsize.width(), 1), qMax(wsize.height(), 1)); + + return sizeForClientSize(wsize, mode, false); +} + +// this helper returns proper size even if the window is shaded +// see also the comment in Client::setGeometry() +QSize AbstractClient::adjustedSize() const +{ + return sizeForClientSize(clientSize()); +} + +/*! + Calculate the appropriate frame size for the given client size \a + wsize. + + \a wsize is adapted according to the window's size hints (minimum, + maximum and incremental size changes). + + */ +QSize Client::sizeForClientSize(const QSize& wsize, Sizemode mode, bool noframe) const +{ + int w = wsize.width(); + int h = wsize.height(); + if (w < 1 || h < 1) { + qCWarning(KWIN_CORE) << "sizeForClientSize() with empty size!" ; + } + if (w < 1) w = 1; + if (h < 1) h = 1; + + // basesize, minsize, maxsize, paspect and resizeinc have all values defined, + // even if they're not set in flags - see getWmNormalHints() + QSize min_size = tabGroup() ? tabGroup()->minSize() : minSize(); + QSize max_size = tabGroup() ? tabGroup()->maxSize() : maxSize(); + if (isDecorated()) { + QSize decominsize(0, 0); + QSize border_size(borderLeft() + borderRight(), borderTop() + borderBottom()); + if (border_size.width() > decominsize.width()) // just in case + decominsize.setWidth(border_size.width()); + if (border_size.height() > decominsize.height()) + decominsize.setHeight(border_size.height()); + if (decominsize.width() > min_size.width()) + min_size.setWidth(decominsize.width()); + if (decominsize.height() > min_size.height()) + min_size.setHeight(decominsize.height()); + } + w = qMin(max_size.width(), w); + h = qMin(max_size.height(), h); + w = qMax(min_size.width(), w); + h = qMax(min_size.height(), h); + + int w1 = w; + int h1 = h; + int width_inc = m_geometryHints.resizeIncrements().width(); + int height_inc = m_geometryHints.resizeIncrements().height(); + int basew_inc = m_geometryHints.baseSize().width(); + int baseh_inc = m_geometryHints.baseSize().height(); + if (!m_geometryHints.hasBaseSize()) { + basew_inc = m_geometryHints.minSize().width(); + baseh_inc = m_geometryHints.minSize().height(); + } + w = int((w - basew_inc) / width_inc) * width_inc + basew_inc; + h = int((h - baseh_inc) / height_inc) * height_inc + baseh_inc; +// code for aspect ratios based on code from FVWM + /* + * The math looks like this: + * + * minAspectX dwidth maxAspectX + * ---------- <= ------- <= ---------- + * minAspectY dheight maxAspectY + * + * If that is multiplied out, then the width and height are + * invalid in the following situations: + * + * minAspectX * dheight > minAspectY * dwidth + * maxAspectX * dheight < maxAspectY * dwidth + * + */ + if (m_geometryHints.hasAspect()) { + double min_aspect_w = m_geometryHints.minAspect().width(); // use doubles, because the values can be MAX_INT + double min_aspect_h = m_geometryHints.minAspect().height(); // and multiplying would go wrong otherwise + double max_aspect_w = m_geometryHints.maxAspect().width(); + double max_aspect_h = m_geometryHints.maxAspect().height(); + // According to ICCCM 4.1.2.3 PMinSize should be a fallback for PBaseSize for size increments, + // but not for aspect ratio. Since this code comes from FVWM, handles both at the same time, + // and I have no idea how it works, let's hope nobody relies on that. + const QSize baseSize = m_geometryHints.baseSize(); + w -= baseSize.width(); + h -= baseSize.height(); + int max_width = max_size.width() - baseSize.width(); + int min_width = min_size.width() - baseSize.width(); + int max_height = max_size.height() - baseSize.height(); + int min_height = min_size.height() - baseSize.height(); +#define ASPECT_CHECK_GROW_W \ + if ( min_aspect_w * h > min_aspect_h * w ) \ + { \ + int delta = int( min_aspect_w * h / min_aspect_h - w ) / width_inc * width_inc; \ + if ( w + delta <= max_width ) \ + w += delta; \ + } +#define ASPECT_CHECK_SHRINK_H_GROW_W \ + if ( min_aspect_w * h > min_aspect_h * w ) \ + { \ + int delta = int( h - w * min_aspect_h / min_aspect_w ) / height_inc * height_inc; \ + if ( h - delta >= min_height ) \ + h -= delta; \ + else \ + { \ + int delta = int( min_aspect_w * h / min_aspect_h - w ) / width_inc * width_inc; \ + if ( w + delta <= max_width ) \ + w += delta; \ + } \ + } +#define ASPECT_CHECK_GROW_H \ + if ( max_aspect_w * h < max_aspect_h * w ) \ + { \ + int delta = int( w * max_aspect_h / max_aspect_w - h ) / height_inc * height_inc; \ + if ( h + delta <= max_height ) \ + h += delta; \ + } +#define ASPECT_CHECK_SHRINK_W_GROW_H \ + if ( max_aspect_w * h < max_aspect_h * w ) \ + { \ + int delta = int( w - max_aspect_w * h / max_aspect_h ) / width_inc * width_inc; \ + if ( w - delta >= min_width ) \ + w -= delta; \ + else \ + { \ + int delta = int( w * max_aspect_h / max_aspect_w - h ) / height_inc * height_inc; \ + if ( h + delta <= max_height ) \ + h += delta; \ + } \ + } + switch(mode) { + case SizemodeAny: +#if 0 // make SizemodeAny equal to SizemodeFixedW - prefer keeping fixed width, + // so that changing aspect ratio to a different value and back keeps the same size (#87298) + { + ASPECT_CHECK_SHRINK_H_GROW_W + ASPECT_CHECK_SHRINK_W_GROW_H + ASPECT_CHECK_GROW_H + ASPECT_CHECK_GROW_W + break; + } +#endif + case SizemodeFixedW: { + // the checks are order so that attempts to modify height are first + ASPECT_CHECK_GROW_H + ASPECT_CHECK_SHRINK_H_GROW_W + ASPECT_CHECK_SHRINK_W_GROW_H + ASPECT_CHECK_GROW_W + break; + } + case SizemodeFixedH: { + ASPECT_CHECK_GROW_W + ASPECT_CHECK_SHRINK_W_GROW_H + ASPECT_CHECK_SHRINK_H_GROW_W + ASPECT_CHECK_GROW_H + break; + } + case SizemodeMax: { + // first checks that try to shrink + ASPECT_CHECK_SHRINK_H_GROW_W + ASPECT_CHECK_SHRINK_W_GROW_H + ASPECT_CHECK_GROW_W + ASPECT_CHECK_GROW_H + break; + } + } +#undef ASPECT_CHECK_SHRINK_H_GROW_W +#undef ASPECT_CHECK_SHRINK_W_GROW_H +#undef ASPECT_CHECK_GROW_W +#undef ASPECT_CHECK_GROW_H + w += baseSize.width(); + h += baseSize.height(); + } + if (!rules()->checkStrictGeometry(!isFullScreen())) { + // disobey increments and aspect by explicit rule + w = w1; + h = h1; + } + + if (!noframe) { + w += borderLeft() + borderRight(); + h += borderTop() + borderBottom(); + } + return rules()->checkSize(QSize(w, h)); +} + +/*! + Gets the client's normal WM hints and reconfigures itself respectively. + */ +void Client::getWmNormalHints() +{ + const bool hadFixedAspect = m_geometryHints.hasAspect(); + // roundtrip to X server + m_geometryHints.fetch(); + m_geometryHints.read(); + + if (!hadFixedAspect && m_geometryHints.hasAspect()) { + // align to eventual new contraints + maximize(max_mode); + } + // Update min/max size of this group + if (tabGroup()) + tabGroup()->updateMinMaxSize(); + + if (isManaged()) { + // update to match restrictions + QSize new_size = adjustedSize(); + if (new_size != size() && !isFullScreen()) { + QRect origClientGeometry(pos() + clientPos(), clientSize()); + resizeWithChecks(new_size); + if ((!isSpecialWindow() || isToolbar()) && !isFullScreen()) { + // try to keep the window in its xinerama screen if possible, + // if that fails at least keep it visible somewhere + QRect area = workspace()->clientArea(MovementArea, this); + if (area.contains(origClientGeometry)) + keepInArea(area); + area = workspace()->clientArea(WorkArea, this); + if (area.contains(origClientGeometry)) + keepInArea(area); + } + } + } + updateAllowedActions(); // affects isResizeable() +} + +QSize Client::minSize() const +{ + return rules()->checkMinSize(m_geometryHints.minSize()); +} + +QSize Client::maxSize() const +{ + return rules()->checkMaxSize(m_geometryHints.maxSize()); +} + +QSize Client::basicUnit() const +{ + return m_geometryHints.resizeIncrements(); +} + +/*! + Auxiliary function to inform the client about the current window + configuration. + + */ +void Client::sendSyntheticConfigureNotify() +{ + xcb_configure_notify_event_t c; + memset(&c, 0, sizeof(c)); + c.response_type = XCB_CONFIGURE_NOTIFY; + c.event = window(); + c.window = window(); + c.x = x() + clientPos().x(); + c.y = y() + clientPos().y(); + c.width = clientSize().width(); + c.height = clientSize().height(); + c.border_width = 0; + c.above_sibling = XCB_WINDOW_NONE; + c.override_redirect = 0; + xcb_send_event(connection(), true, c.event, XCB_EVENT_MASK_STRUCTURE_NOTIFY, reinterpret_cast(&c)); + xcb_flush(connection()); +} + +const QPoint Client::calculateGravitation(bool invert, int gravity) const +{ + int dx, dy; + dx = dy = 0; + + if (gravity == 0) // default (nonsense) value for the argument + gravity = m_geometryHints.windowGravity(); + +// dx, dy specify how the client window moves to make space for the frame + switch(gravity) { + case NorthWestGravity: // move down right + default: + dx = borderLeft(); + dy = borderTop(); + break; + case NorthGravity: // move right + dx = 0; + dy = borderTop(); + break; + case NorthEastGravity: // move down left + dx = -borderRight(); + dy = borderTop(); + break; + case WestGravity: // move right + dx = borderLeft(); + dy = 0; + break; + case CenterGravity: + break; // will be handled specially + case StaticGravity: // don't move + dx = 0; + dy = 0; + break; + case EastGravity: // move left + dx = -borderRight(); + dy = 0; + break; + case SouthWestGravity: // move up right + dx = borderLeft() ; + dy = -borderBottom(); + break; + case SouthGravity: // move up + dx = 0; + dy = -borderBottom(); + break; + case SouthEastGravity: // move up left + dx = -borderRight(); + dy = -borderBottom(); + break; + } + if (gravity != CenterGravity) { + // translate from client movement to frame movement + dx -= borderLeft(); + dy -= borderTop(); + } else { + // center of the frame will be at the same position client center without frame would be + dx = - (borderLeft() + borderRight()) / 2; + dy = - (borderTop() + borderBottom()) / 2; + } + if (!invert) + return QPoint(x() + dx, y() + dy); + else + return QPoint(x() - dx, y() - dy); +} + +void Client::configureRequest(int value_mask, int rx, int ry, int rw, int rh, int gravity, bool from_tool) +{ + // "maximized" is a user setting -> we do not allow the client to resize itself + // away from this & against the users explicit wish + qCDebug(KWIN_CORE) << this << bool(value_mask & (CWX|CWWidth|CWY|CWHeight)) << + bool(maximizeMode() & MaximizeVertical) << + bool(maximizeMode() & MaximizeHorizontal); + + // we want to (partially) ignore the request when the window is somehow maximized or quicktiled + bool ignore = !app_noborder && (quickTileMode() != QuickTileMode(QuickTileFlag::None) || maximizeMode() != MaximizeRestore); + // however, the user shall be able to force obedience despite and also disobedience in general + ignore = rules()->checkIgnoreGeometry(ignore); + if (!ignore) { // either we're not max'd / q'tiled or the user allowed the client to break that - so break it. + updateQuickTileMode(QuickTileFlag::None); + max_mode = MaximizeRestore; + emit quickTileModeChanged(); + } else if (!app_noborder && quickTileMode() == QuickTileMode(QuickTileFlag::None) && + (maximizeMode() == MaximizeVertical || maximizeMode() == MaximizeHorizontal)) { + // ignoring can be, because either we do, or the user does explicitly not want it. + // for partially maximized windows we want to allow configures in the other dimension. + // so we've to ask the user again - to know whether we just ignored for the partial maximization. + // the problem here is, that the user can explicitly permit configure requests - even for maximized windows! + // we cannot distinguish that from passing "false" for partially maximized windows. + ignore = rules()->checkIgnoreGeometry(false); + if (!ignore) { // the user is not interested, so we fix up dimensions + if (maximizeMode() == MaximizeVertical) + value_mask &= ~(CWY|CWHeight); + if (maximizeMode() == MaximizeHorizontal) + value_mask &= ~(CWX|CWWidth); + if (!(value_mask & (CWX|CWWidth|CWY|CWHeight))) { + ignore = true; // the modification turned the request void + } + } + } + + if (ignore) { + qCDebug(KWIN_CORE) << "DENIED"; + return; // nothing to (left) to do for use - bugs #158974, #252314, #321491 + } + + qCDebug(KWIN_CORE) << "PERMITTED" << this << bool(value_mask & (CWX|CWWidth|CWY|CWHeight)); + + if (gravity == 0) // default (nonsense) value for the argument + gravity = m_geometryHints.windowGravity(); + if (value_mask & (CWX | CWY)) { + QPoint new_pos = calculateGravitation(true, gravity); // undo gravitation + if (value_mask & CWX) + new_pos.setX(rx); + if (value_mask & CWY) + new_pos.setY(ry); + + // clever(?) workaround for applications like xv that want to set + // the location to the current location but miscalculate the + // frame size due to kwin being a double-reparenting window + // manager + if (new_pos.x() == x() + clientPos().x() && new_pos.y() == y() + clientPos().y() + && gravity == NorthWestGravity && !from_tool) { + new_pos.setX(x()); + new_pos.setY(y()); + } + + int nw = clientSize().width(); + int nh = clientSize().height(); + if (value_mask & CWWidth) + nw = rw; + if (value_mask & CWHeight) + nh = rh; + QSize ns = sizeForClientSize(QSize(nw, nh)); // enforces size if needed + new_pos = rules()->checkPosition(new_pos); + int newScreen = screens()->number(QRect(new_pos, ns).center()); + if (newScreen != rules()->checkScreen(newScreen)) + return; // not allowed by rule + + QRect origClientGeometry(pos() + clientPos(), clientSize()); + GeometryUpdatesBlocker blocker(this); + move(new_pos); + plainResize(ns); + setGeometry(QRect(calculateGravitation(false, gravity), size())); + updateFullScreenHack(QRect(new_pos, QSize(nw, nh))); + QRect area = workspace()->clientArea(WorkArea, this); + if (!from_tool && (!isSpecialWindow() || isToolbar()) && !isFullScreen() + && area.contains(origClientGeometry)) + keepInArea(area); + + // this is part of the kicker-xinerama-hack... it should be + // safe to remove when kicker gets proper ExtendedStrut support; + // see Workspace::updateClientArea() and + // Client::adjustedClientArea() + if (hasStrut()) + workspace() -> updateClientArea(); + } + + if (value_mask & (CWWidth | CWHeight) + && !(value_mask & (CWX | CWY))) { // pure resize + int nw = clientSize().width(); + int nh = clientSize().height(); + if (value_mask & CWWidth) + nw = rw; + if (value_mask & CWHeight) + nh = rh; + QSize ns = sizeForClientSize(QSize(nw, nh)); + + if (ns != size()) { // don't restore if some app sets its own size again + QRect origClientGeometry(pos() + clientPos(), clientSize()); + GeometryUpdatesBlocker blocker(this); + resizeWithChecks(ns, xcb_gravity_t(gravity)); + updateFullScreenHack(QRect(calculateGravitation(true, m_geometryHints.windowGravity()), QSize(nw, nh))); + if (!from_tool && (!isSpecialWindow() || isToolbar()) && !isFullScreen()) { + // try to keep the window in its xinerama screen if possible, + // if that fails at least keep it visible somewhere + QRect area = workspace()->clientArea(MovementArea, this); + if (area.contains(origClientGeometry)) + keepInArea(area); + area = workspace()->clientArea(WorkArea, this); + if (area.contains(origClientGeometry)) + keepInArea(area); + } + } + } + geom_restore = geometry(); + // No need to send synthetic configure notify event here, either it's sent together + // with geometry change, or there's no need to send it. + // Handling of the real ConfigureRequest event forces sending it, as there it's necessary. +} + +void Client::resizeWithChecks(int w, int h, xcb_gravity_t gravity, ForceGeometry_t force) +{ + assert(!shade_geometry_change); + if (isShade()) { + if (h == borderTop() + borderBottom()) { + qCWarning(KWIN_CORE) << "Shaded geometry passed for size:" ; + } + } + int newx = x(); + int newy = y(); + QRect area = workspace()->clientArea(WorkArea, this); + // don't allow growing larger than workarea + if (w > area.width()) + w = area.width(); + if (h > area.height()) + h = area.height(); + QSize tmp = adjustedSize(QSize(w, h)); // checks size constraints, including min/max size + w = tmp.width(); + h = tmp.height(); + if (gravity == 0) { + gravity = m_geometryHints.windowGravity(); + } + switch(gravity) { + case NorthWestGravity: // top left corner doesn't move + default: + break; + case NorthGravity: // middle of top border doesn't move + newx = (newx + width() / 2) - (w / 2); + break; + case NorthEastGravity: // top right corner doesn't move + newx = newx + width() - w; + break; + case WestGravity: // middle of left border doesn't move + newy = (newy + height() / 2) - (h / 2); + break; + case CenterGravity: // middle point doesn't move + newx = (newx + width() / 2) - (w / 2); + newy = (newy + height() / 2) - (h / 2); + break; + case StaticGravity: // top left corner of _client_ window doesn't move + // since decoration doesn't change, equal to NorthWestGravity + break; + case EastGravity: // // middle of right border doesn't move + newx = newx + width() - w; + newy = (newy + height() / 2) - (h / 2); + break; + case SouthWestGravity: // bottom left corner doesn't move + newy = newy + height() - h; + break; + case SouthGravity: // middle of bottom border doesn't move + newx = (newx + width() / 2) - (w / 2); + newy = newy + height() - h; + break; + case SouthEastGravity: // bottom right corner doesn't move + newx = newx + width() - w; + newy = newy + height() - h; + break; + } + setGeometry(newx, newy, w, h, force); +} + +// _NET_MOVERESIZE_WINDOW +void Client::NETMoveResizeWindow(int flags, int x, int y, int width, int height) +{ + int gravity = flags & 0xff; + int value_mask = 0; + if (flags & (1 << 8)) + value_mask |= CWX; + if (flags & (1 << 9)) + value_mask |= CWY; + if (flags & (1 << 10)) + value_mask |= CWWidth; + if (flags & (1 << 11)) + value_mask |= CWHeight; + configureRequest(value_mask, x, y, width, height, gravity, true); +} + +/*! + Returns whether the window is moveable or has a fixed + position. + */ +bool Client::isMovable() const +{ + if (!hasNETSupport() && !m_motif.move()) { + return false; + } + if (isFullScreen()) + return false; + if (isSpecialWindow() && !isSplash() && !isToolbar()) // allow moving of splashscreens :) + return false; + if (rules()->checkPosition(invalidPoint) != invalidPoint) // forced position + return false; + return true; +} + +/*! + Returns whether the window is moveable across Xinerama screens + */ +bool Client::isMovableAcrossScreens() const +{ + if (!hasNETSupport() && !m_motif.move()) { + return false; + } + if (isSpecialWindow() && !isSplash() && !isToolbar()) // allow moving of splashscreens :) + return false; + if (rules()->checkPosition(invalidPoint) != invalidPoint) // forced position + return false; + return true; +} + +/*! + Returns whether the window is resizable or has a fixed size. + */ +bool Client::isResizable() const +{ + if (!hasNETSupport() && !m_motif.resize()) { + return false; + } + if (isFullScreen()) + return false; + if (isSpecialWindow() || isSplash() || isToolbar()) + return false; + if (rules()->checkSize(QSize()).isValid()) // forced size + return false; + const Position mode = moveResizePointerMode(); + if ((mode == PositionTop || mode == PositionTopLeft || mode == PositionTopRight || + mode == PositionLeft || mode == PositionBottomLeft) && rules()->checkPosition(invalidPoint) != invalidPoint) + return false; + + QSize min = tabGroup() ? tabGroup()->minSize() : minSize(); + QSize max = tabGroup() ? tabGroup()->maxSize() : maxSize(); + return min.width() < max.width() || min.height() < max.height(); +} + +/* + Returns whether the window is maximizable or not + */ +bool Client::isMaximizable() const +{ + if (!isResizable() || isToolbar()) // SELI isToolbar() ? + return false; + if (rules()->checkMaximize(MaximizeRestore) == MaximizeRestore && rules()->checkMaximize(MaximizeFull) != MaximizeRestore) + return true; + return false; +} + + +/*! + Reimplemented to inform the client about the new window position. + */ +void Client::setGeometry(int x, int y, int w, int h, ForceGeometry_t force) +{ + // this code is also duplicated in Client::plainResize() + // Ok, the shading geometry stuff. Generally, code doesn't care about shaded geometry, + // simply because there are too many places dealing with geometry. Those places + // ignore shaded state and use normal geometry, which they usually should get + // from adjustedSize(). Such geometry comes here, and if the window is shaded, + // the geometry is used only for client_size, since that one is not used when + // shading. Then the frame geometry is adjusted for the shaded geometry. + // This gets more complicated in the case the code does only something like + // setGeometry( geometry()) - geometry() will return the shaded frame geometry. + // Such code is wrong and should be changed to handle the case when the window is shaded, + // for example using Client::clientSize() + + if (shade_geometry_change) + ; // nothing + else if (isShade()) { + if (h == borderTop() + borderBottom()) { + qCDebug(KWIN_CORE) << "Shaded geometry passed for size:"; + } else { + client_size = QSize(w - borderLeft() - borderRight(), h - borderTop() - borderBottom()); + h = borderTop() + borderBottom(); + } + } else { + client_size = QSize(w - borderLeft() - borderRight(), h - borderTop() - borderBottom()); + } + QRect g(x, y, w, h); + if (!areGeometryUpdatesBlocked() && g != rules()->checkGeometry(g)) { + qCDebug(KWIN_CORE) << "forced geometry fail:" << g << ":" << rules()->checkGeometry(g); + } + if (force == NormalGeometrySet && geom == g && pendingGeometryUpdate() == PendingGeometryNone) + return; + geom = g; + if (areGeometryUpdatesBlocked()) { + if (pendingGeometryUpdate() == PendingGeometryForced) + {} // maximum, nothing needed + else if (force == ForceGeometrySet) + setPendingGeometryUpdate(PendingGeometryForced); + else + setPendingGeometryUpdate(PendingGeometryNormal); + return; + } + QSize oldClientSize = m_frame.geometry().size(); + bool resized = (geometryBeforeUpdateBlocking().size() != geom.size() || pendingGeometryUpdate() == PendingGeometryForced); + if (resized) { + resizeDecoration(); + m_frame.setGeometry(x, y, w, h); + if (!isShade()) { + QSize cs = clientSize(); + m_wrapper.setGeometry(QRect(clientPos(), cs)); + if (!isResize() || syncRequest.counter == XCB_NONE) + m_client.setGeometry(0, 0, cs.width(), cs.height()); + // SELI - won't this be too expensive? + // THOMAS - yes, but gtk+ clients will not resize without ... + sendSyntheticConfigureNotify(); + } + updateShape(); + } else { + if (isMoveResize()) { + if (compositing()) // Defer the X update until we leave this mode + needsXWindowMove = true; + else + m_frame.move(x, y); // sendSyntheticConfigureNotify() on finish shall be sufficient + } else { + m_frame.move(x, y); + sendSyntheticConfigureNotify(); + } + + // Unconditionally move the input window: it won't affect rendering + m_decoInputExtent.move(QPoint(x, y) + inputPos()); + } + updateWindowRules(Rules::Position|Rules::Size); + + // keep track of old maximize mode + // to detect changes + screens()->setCurrent(this); + workspace()->updateStackingOrder(); + + // need to regenerate decoration pixmaps when either + // - size is changed + // - maximize mode is changed to MaximizeRestore, when size unchanged + // which can happen when untabbing maximized windows + if (resized) { + if (oldClientSize != QSize(w,h)) + discardWindowPixmap(); + } + emit geometryShapeChanged(this, geometryBeforeUpdateBlocking()); + addRepaintDuringGeometryUpdates(); + updateGeometryBeforeUpdateBlocking(); + + // Update states of all other windows in this group + if (tabGroup()) + tabGroup()->updateStates(this, TabGroup::Geometry); + + // TODO: this signal is emitted too often + emit geometryChanged(); +} + +void Client::plainResize(int w, int h, ForceGeometry_t force) +{ + // this code is also duplicated in Client::setGeometry(), and it's also commented there + if (shade_geometry_change) + ; // nothing + else if (isShade()) { + if (h == borderTop() + borderBottom()) { + qCDebug(KWIN_CORE) << "Shaded geometry passed for size:"; + } else { + client_size = QSize(w - borderLeft() - borderRight(), h - borderTop() - borderBottom()); + h = borderTop() + borderBottom(); + } + } else { + client_size = QSize(w - borderLeft() - borderRight(), h - borderTop() - borderBottom()); + } + QSize s(w, h); + if (!areGeometryUpdatesBlocked() && s != rules()->checkSize(s)) { + qCDebug(KWIN_CORE) << "forced size fail:" << s << ":" << rules()->checkSize(s); + } + // resuming geometry updates is handled only in setGeometry() + assert(pendingGeometryUpdate() == PendingGeometryNone || areGeometryUpdatesBlocked()); + if (force == NormalGeometrySet && geom.size() == s) + return; + geom.setSize(s); + if (areGeometryUpdatesBlocked()) { + if (pendingGeometryUpdate() == PendingGeometryForced) + {} // maximum, nothing needed + else if (force == ForceGeometrySet) + setPendingGeometryUpdate(PendingGeometryForced); + else + setPendingGeometryUpdate(PendingGeometryNormal); + return; + } + QSize oldClientSize = m_frame.geometry().size(); + resizeDecoration(); + m_frame.resize(w, h); +// resizeDecoration( s ); + if (!isShade()) { + QSize cs = clientSize(); + m_wrapper.setGeometry(QRect(clientPos(), cs)); + m_client.setGeometry(0, 0, cs.width(), cs.height()); + } + updateShape(); + + sendSyntheticConfigureNotify(); + updateWindowRules(Rules::Position|Rules::Size); + screens()->setCurrent(this); + workspace()->updateStackingOrder(); + if (oldClientSize != QSize(w,h)) + discardWindowPixmap(); + emit geometryShapeChanged(this, geometryBeforeUpdateBlocking()); + addRepaintDuringGeometryUpdates(); + updateGeometryBeforeUpdateBlocking(); + + // Update states of all other windows in this group + if (tabGroup()) + tabGroup()->updateStates(this, TabGroup::Geometry); + // TODO: this signal is emitted too often + emit geometryChanged(); +} + +/*! + Reimplemented to inform the client about the new window position. + */ +void AbstractClient::move(int x, int y, ForceGeometry_t force) +{ + // resuming geometry updates is handled only in setGeometry() + 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 && geom.topLeft() == p) + return; + geom.moveTopLeft(p); + if (areGeometryUpdatesBlocked()) { + if (pendingGeometryUpdate() == PendingGeometryForced) + {} // maximum, nothing needed + else if (force == ForceGeometrySet) + setPendingGeometryUpdate(PendingGeometryForced); + else + setPendingGeometryUpdate(PendingGeometryNormal); + return; + } + doMove(x, y); + updateWindowRules(Rules::Position); + screens()->setCurrent(this); + workspace()->updateStackingOrder(); + // client itself is not damaged + addRepaintDuringGeometryUpdates(); + updateGeometryBeforeUpdateBlocking(); + + // Update states of all other windows in this group + updateTabGroupStates(TabGroup::Geometry); + emit geometryChanged(); +} + +void Client::doMove(int x, int y) +{ + m_frame.move(x, y); + sendSyntheticConfigureNotify(); +} + +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()) + setGeometry(QRect(pos(), adjustedSize()), NormalGeometrySet); + else + setGeometry(geometry(), NormalGeometrySet); + m_pendingGeometryUpdate = PendingGeometryNone; + } + } + } +} + +void AbstractClient::maximize(MaximizeMode m) +{ + if (m == maximizeMode()) { + return; + } + setMaximize(m & MaximizeVertical, m & MaximizeHorizontal); +} + +/*! + Sets the maximization according to \a vertically and \a horizontally + */ +void AbstractClient::setMaximize(bool vertically, bool horizontally) +{ + // changeMaximize() flips the state, so change from set->flip + const MaximizeMode oldMode = maximizeMode(); + changeMaximize( + oldMode & MaximizeVertical ? !vertically : vertically, + oldMode & MaximizeHorizontal ? !horizontally : horizontally, + false); + const MaximizeMode newMode = maximizeMode(); + if (oldMode != newMode) { + emit clientMaximizedStateChanged(this, newMode); + emit clientMaximizedStateChanged(this, vertically, horizontally); + } + +} + +// Update states of all other windows in this group +class TabSynchronizer +{ +public: + TabSynchronizer(AbstractClient *client, TabGroup::States syncStates) : + m_client(client) , m_states(syncStates) + { + if (client->tabGroup()) + client->tabGroup()->blockStateUpdates(true); + } + ~TabSynchronizer() + { + syncNow(); + } + void syncNow() + { + if (m_client && m_client->tabGroup()) { + m_client->tabGroup()->blockStateUpdates(false); + m_client->tabGroup()->updateStates(dynamic_cast(m_client), m_states); + } + m_client = 0; + } +private: + AbstractClient *m_client; + TabGroup::States m_states; +}; + + +static bool changeMaximizeRecursion = false; +void Client::changeMaximize(bool vertical, bool horizontal, bool adjust) +{ + if (changeMaximizeRecursion) + return; + + if (!isResizable() || isToolbar()) // SELI isToolbar() ? + return; + + QRect clientArea; + if (isElectricBorderMaximizing()) + clientArea = workspace()->clientArea(MaximizeArea, Cursor::pos(), desktop()); + else + clientArea = workspace()->clientArea(MaximizeArea, this); + + MaximizeMode old_mode = max_mode; + // 'adjust == true' means to update the size only, e.g. after changing workspace size + if (!adjust) { + if (vertical) + max_mode = MaximizeMode(max_mode ^ MaximizeVertical); + if (horizontal) + max_mode = MaximizeMode(max_mode ^ MaximizeHorizontal); + } + + // if the client insist on a fix aspect ratio, we check whether the maximizing will get us + // out of screen bounds and take that as a "full maximization with aspect check" then + if (m_geometryHints.hasAspect() && // fixed aspect + (max_mode == MaximizeVertical || max_mode == MaximizeHorizontal) && // ondimensional maximization + rules()->checkStrictGeometry(true)) { // obey aspect + const QSize minAspect = m_geometryHints.minAspect(); + const QSize maxAspect = m_geometryHints.maxAspect(); + if (max_mode == MaximizeVertical || (old_mode & MaximizeVertical)) { + const double fx = minAspect.width(); // use doubles, because the values can be MAX_INT + const double fy = maxAspect.height(); // use doubles, because the values can be MAX_INT + if (fx*clientArea.height()/fy > clientArea.width()) // too big + max_mode = old_mode & MaximizeHorizontal ? MaximizeRestore : MaximizeFull; + } else { // max_mode == MaximizeHorizontal + const double fx = maxAspect.width(); + const double fy = minAspect.height(); + if (fy*clientArea.width()/fx > clientArea.height()) // too big + max_mode = old_mode & MaximizeVertical ? MaximizeRestore : MaximizeFull; + } + } + + max_mode = rules()->checkMaximize(max_mode); + if (!adjust && max_mode == old_mode) + return; + + GeometryUpdatesBlocker blocker(this); + // QT synchronizing required because we eventually change from QT to Maximized + TabSynchronizer syncer(this, TabGroup::Maximized|TabGroup::QuickTile); + + // maximing one way and unmaximizing the other way shouldn't happen, + // so restore first and then maximize the other way + if ((old_mode == MaximizeVertical && max_mode == MaximizeHorizontal) + || (old_mode == MaximizeHorizontal && max_mode == MaximizeVertical)) { + changeMaximize(false, false, false); // restore + } + + // save sizes for restoring, if maximalizing + QSize sz; + if (isShade()) + sz = sizeForClientSize(clientSize()); + else + sz = size(); + + if (quickTileMode() == QuickTileMode(QuickTileFlag::None)) { + if (!adjust && !(old_mode & MaximizeVertical)) { + geom_restore.setTop(y()); + geom_restore.setHeight(sz.height()); + } + if (!adjust && !(old_mode & MaximizeHorizontal)) { + geom_restore.setLeft(x()); + geom_restore.setWidth(sz.width()); + } + } + + // call into decoration update borders + if (isDecorated() && decoration()->client() && !(options->borderlessMaximizedWindows() && max_mode == KWin::MaximizeFull)) { + changeMaximizeRecursion = true; + const auto c = decoration()->client().data(); + if ((max_mode & MaximizeVertical) != (old_mode & MaximizeVertical)) { + emit c->maximizedVerticallyChanged(max_mode & MaximizeVertical); + } + if ((max_mode & MaximizeHorizontal) != (old_mode & MaximizeHorizontal)) { + emit c->maximizedHorizontallyChanged(max_mode & MaximizeHorizontal); + } + if ((max_mode == MaximizeFull) != (old_mode == MaximizeFull)) { + emit c->maximizedChanged(max_mode & MaximizeFull); + } + changeMaximizeRecursion = false; + } + + if (options->borderlessMaximizedWindows()) { + // triggers a maximize change. + // The next setNoBorder interation will exit since there's no change but the first recursion pullutes the restore geometry + changeMaximizeRecursion = true; + setNoBorder(rules()->checkNoBorder(app_noborder || (m_motif.hasDecoration() && m_motif.noBorder()) || max_mode == MaximizeFull)); + changeMaximizeRecursion = false; + } + + const ForceGeometry_t geom_mode = isDecorated() ? ForceGeometrySet : NormalGeometrySet; + + // Conditional quick tiling exit points + if (quickTileMode() != QuickTileMode(QuickTileFlag::None)) { + if (old_mode == MaximizeFull && + !clientArea.contains(geom_restore.center())) { + // Not restoring on the same screen + // TODO: The following doesn't work for some reason + //quick_tile_mode = QuickTileFlag::None; // And exit quick tile mode manually + } else if ((old_mode == MaximizeVertical && max_mode == MaximizeRestore) || + (old_mode == MaximizeFull && max_mode == MaximizeHorizontal)) { + // Modifying geometry of a tiled window + updateQuickTileMode(QuickTileFlag::None); // Exit quick tile mode without restoring geometry + } + } + + switch(max_mode) { + + case MaximizeVertical: { + if (old_mode & MaximizeHorizontal) { // actually restoring from MaximizeFull + if (geom_restore.width() == 0 || !clientArea.contains(geom_restore.center())) { + // needs placement + plainResize(adjustedSize(QSize(width() * 2 / 3, clientArea.height()), SizemodeFixedH), geom_mode); + Placement::self()->placeSmart(this, clientArea); + } else { + setGeometry(QRect(QPoint(geom_restore.x(), clientArea.top()), + adjustedSize(QSize(geom_restore.width(), clientArea.height()), SizemodeFixedH)), geom_mode); + } + } else { + QRect r(x(), clientArea.top(), width(), clientArea.height()); + r.setTopLeft(rules()->checkPosition(r.topLeft())); + r.setSize(adjustedSize(r.size(), SizemodeFixedH)); + setGeometry(r, geom_mode); + } + info->setState(NET::MaxVert, NET::Max); + break; + } + + case MaximizeHorizontal: { + if (old_mode & MaximizeVertical) { // actually restoring from MaximizeFull + if (geom_restore.height() == 0 || !clientArea.contains(geom_restore.center())) { + // needs placement + plainResize(adjustedSize(QSize(clientArea.width(), height() * 2 / 3), SizemodeFixedW), geom_mode); + Placement::self()->placeSmart(this, clientArea); + } else { + setGeometry(QRect(QPoint(clientArea.left(), geom_restore.y()), + adjustedSize(QSize(clientArea.width(), geom_restore.height()), SizemodeFixedW)), geom_mode); + } + } else { + QRect r(clientArea.left(), y(), clientArea.width(), height()); + r.setTopLeft(rules()->checkPosition(r.topLeft())); + r.setSize(adjustedSize(r.size(), SizemodeFixedW)); + setGeometry(r, geom_mode); + } + info->setState(NET::MaxHoriz, NET::Max); + break; + } + + case MaximizeRestore: { + QRect restore = geometry(); + // when only partially maximized, geom_restore may not have the other dimension remembered + if (old_mode & MaximizeVertical) { + restore.setTop(geom_restore.top()); + restore.setBottom(geom_restore.bottom()); + } + if (old_mode & MaximizeHorizontal) { + restore.setLeft(geom_restore.left()); + restore.setRight(geom_restore.right()); + } + if (!restore.isValid()) { + QSize s = QSize(clientArea.width() * 2 / 3, clientArea.height() * 2 / 3); + if (geom_restore.width() > 0) + s.setWidth(geom_restore.width()); + if (geom_restore.height() > 0) + s.setHeight(geom_restore.height()); + plainResize(adjustedSize(s)); + Placement::self()->placeSmart(this, clientArea); + restore = geometry(); + if (geom_restore.width() > 0) + restore.moveLeft(geom_restore.x()); + if (geom_restore.height() > 0) + restore.moveTop(geom_restore.y()); + geom_restore = restore; // relevant for mouse pos calculation, bug #298646 + } + if (m_geometryHints.hasAspect()) { + restore.setSize(adjustedSize(restore.size(), SizemodeAny)); + } + setGeometry(restore, geom_mode); + if (!clientArea.contains(geom_restore.center())) // Not restoring to the same screen + Placement::self()->place(this, clientArea); + info->setState(0, NET::Max); + updateQuickTileMode(QuickTileFlag::None); + break; + } + + case MaximizeFull: { + QRect r(clientArea); + r.setTopLeft(rules()->checkPosition(r.topLeft())); + r.setSize(adjustedSize(r.size(), SizemodeMax)); + if (r.size() != clientArea.size()) { // to avoid off-by-one errors... + if (isElectricBorderMaximizing() && r.width() < clientArea.width()) { + r.moveLeft(qMax(clientArea.left(), Cursor::pos().x() - r.width()/2)); + r.moveRight(qMin(clientArea.right(), r.right())); + } else { + r.moveCenter(clientArea.center()); + const bool closeHeight = r.height() > 97*clientArea.height()/100; + const bool closeWidth = r.width() > 97*clientArea.width() /100; + const bool overHeight = r.height() > clientArea.height(); + const bool overWidth = r.width() > clientArea.width(); + if (closeWidth || closeHeight) { + Position titlePos = titlebarPosition(); + const QRect screenArea = workspace()->clientArea(ScreenArea, clientArea.center(), desktop()); + if (closeHeight) { + bool tryBottom = titlePos == PositionBottom; + if ((overHeight && titlePos == PositionTop) || + screenArea.top() == clientArea.top()) + r.setTop(clientArea.top()); + else + tryBottom = true; + if (tryBottom && + (overHeight || screenArea.bottom() == clientArea.bottom())) + r.setBottom(clientArea.bottom()); + } + if (closeWidth) { + bool tryLeft = titlePos == PositionLeft; + if ((overWidth && titlePos == PositionRight) || + screenArea.right() == clientArea.right()) + r.setRight(clientArea.right()); + else + tryLeft = true; + if (tryLeft && (overWidth || screenArea.left() == clientArea.left())) + r.setLeft(clientArea.left()); + } + } + } + r.moveTopLeft(rules()->checkPosition(r.topLeft())); + } + setGeometry(r, geom_mode); + if (options->electricBorderMaximize() && r.top() == clientArea.top()) + updateQuickTileMode(QuickTileFlag::Maximize); + else + updateQuickTileMode(QuickTileFlag::None); + info->setState(NET::Max, NET::Max); + break; + } + default: + break; + } + + syncer.syncNow(); // important because of window rule updates! + + updateAllowedActions(); + updateWindowRules(Rules::MaximizeVert|Rules::MaximizeHoriz|Rules::Position|Rules::Size); + emit quickTileModeChanged(); +} + +bool AbstractClient::isFullScreenable() const +{ + return isFullScreenable(false); +} + +bool AbstractClient::isFullScreenable(bool fullscreen_hack) const +{ + if (!rules()->checkFullScreen(true)) + return false; + if (fullscreen_hack) + return isNormalWindow(); + if (rules()->checkStrictGeometry(true)) { // allow rule to ignore geometry constraints + QRect fsarea = workspace()->clientArea(FullScreenArea, this); + if (sizeForClientSize(fsarea.size(), SizemodeAny, true) != fsarea.size()) + return false; // the app wouldn't fit exactly fullscreen geometry due to its strict geometry requirements + } + // don't check size constrains - some apps request fullscreen despite requesting fixed size + return !isSpecialWindow(); // also better disallow only weird types to go fullscreen +} + +bool Client::userCanSetFullScreen() const +{ + if (fullscreen_mode == FullScreenHack) + return false; + if (!isFullScreenable(false)) + return false; + return isNormalWindow() || isDialog(); +} + +void Client::setFullScreen(bool set, bool user) +{ + if (!isFullScreen() && !set) + return; + if (fullscreen_mode == FullScreenHack) + return; + if (user && !userCanSetFullScreen()) + return; + set = rules()->checkFullScreen(set && !isSpecialWindow()); + setShade(ShadeNone); + bool was_fs = isFullScreen(); + if (was_fs) + workspace()->updateFocusMousePosition(Cursor::pos()); // may cause leave event + else + geom_fs_restore = geometry(); + fullscreen_mode = set ? FullScreenNormal : FullScreenNone; + if (was_fs == isFullScreen()) + return; + if (set) { + untab(); + workspace()->raiseClient(this); + } + StackingUpdatesBlocker blocker1(workspace()); + GeometryUpdatesBlocker blocker2(this); + workspace()->updateClientLayer(this); // active fullscreens get different layer + info->setState(isFullScreen() ? NET::FullScreen : NET::States(0), NET::FullScreen); + updateDecoration(false, false); + if (isFullScreen()) { + if (info->fullscreenMonitors().isSet()) + setGeometry(fullscreenMonitorsArea(info->fullscreenMonitors())); + else + setGeometry(workspace()->clientArea(FullScreenArea, this)); + } + else { + if (!geom_fs_restore.isNull()) { + int currentScreen = screen(); + setGeometry(QRect(geom_fs_restore.topLeft(), adjustedSize(geom_fs_restore.size()))); + if( currentScreen != screen()) + workspace()->sendClientToScreen( this, currentScreen ); + // TODO isShaded() ? + } else { + // does this ever happen? + setGeometry(workspace()->clientArea(MaximizeArea, this)); + } + } + updateWindowRules(Rules::Fullscreen|Rules::Position|Rules::Size); + + if (was_fs != isFullScreen()) { + emit clientFullScreenSet(this, set, user); + emit fullScreenChanged(); + } +} + + +void Client::updateFullscreenMonitors(NETFullscreenMonitors topology) +{ + int nscreens = screens()->count(); + +// qDebug() << "incoming request with top: " << topology.top << " bottom: " << topology.bottom +// << " left: " << topology.left << " right: " << topology.right +// << ", we have: " << nscreens << " screens."; + + if (topology.top >= nscreens || + topology.bottom >= nscreens || + topology.left >= nscreens || + topology.right >= nscreens) { + qCWarning(KWIN_CORE) << "fullscreenMonitors update failed. request higher than number of screens."; + return; + } + + info->setFullscreenMonitors(topology); + if (isFullScreen()) + setGeometry(fullscreenMonitorsArea(topology)); +} + + +/*! + Calculates the bounding rectangle defined by the 4 monitor indices indicating the + top, bottom, left, and right edges of the window when the fullscreen state is enabled. + */ +QRect Client::fullscreenMonitorsArea(NETFullscreenMonitors requestedTopology) const +{ + QRect top, bottom, left, right, total; + + top = screens()->geometry(requestedTopology.top); + bottom = screens()->geometry(requestedTopology.bottom); + left = screens()->geometry(requestedTopology.left); + right = screens()->geometry(requestedTopology.right); + total = top.united(bottom.united(left.united(right))); + +// qDebug() << "top: " << top << " bottom: " << bottom +// << " left: " << left << " right: " << right; +// qDebug() << "returning rect: " << total; + return total; +} + + +int Client::checkFullScreenHack(const QRect& geom) const +{ + if (!options->isLegacyFullscreenSupport()) + return 0; + // if it's noborder window, and has size of one screen or the whole desktop geometry, it's fullscreen hack + if (noBorder() && app_noborder && isFullScreenable(true)) { + if (geom.size() == workspace()->clientArea(FullArea, geom.center(), desktop()).size()) + return 2; // full area fullscreen hack + if (geom.size() == workspace()->clientArea(ScreenArea, geom.center(), desktop()).size()) + return 1; // xinerama-aware fullscreen hack + } + return 0; +} + +void Client::updateFullScreenHack(const QRect& geom) +{ + int type = checkFullScreenHack(geom); + if (fullscreen_mode == FullScreenNone && type != 0) { + fullscreen_mode = FullScreenHack; + updateDecoration(false, false); + QRect geom; + if (rules()->checkStrictGeometry(false)) { + geom = type == 2 // 1 - it's xinerama-aware fullscreen hack, 2 - it's full area + ? workspace()->clientArea(FullArea, geom.center(), desktop()) + : workspace()->clientArea(ScreenArea, geom.center(), desktop()); + } else + geom = workspace()->clientArea(FullScreenArea, geom.center(), desktop()); + setGeometry(geom); + emit fullScreenChanged(); + } else if (fullscreen_mode == FullScreenHack && type == 0) { + fullscreen_mode = FullScreenNone; + updateDecoration(false, false); + // whoever called this must setup correct geometry + emit fullScreenChanged(); + } + StackingUpdatesBlocker blocker(workspace()); + workspace()->updateClientLayer(this); // active fullscreens get different layer +} + +static GeometryTip* geometryTip = 0; + +void Client::positionGeometryTip() +{ + assert(isMove() || isResize()); + // Position and Size display + if (effects && static_cast(effects)->provides(Effect::GeometryTip)) + return; // some effect paints this for us + if (options->showGeometryTip()) { + if (!geometryTip) { + geometryTip = new GeometryTip(&m_geometryHints); + } + QRect wgeom(moveResizeGeometry()); // position of the frame, size of the window itself + wgeom.setWidth(wgeom.width() - (width() - clientSize().width())); + wgeom.setHeight(wgeom.height() - (height() - clientSize().height())); + if (isShade()) + wgeom.setHeight(0); + geometryTip->setGeometry(wgeom); + if (!geometryTip->isVisible()) + geometryTip->show(); + geometryTip->raise(); + } +} + +bool AbstractClient::startMoveResize() +{ + assert(!isMoveResize()); + assert(QWidget::keyboardGrabber() == NULL); + assert(QWidget::mouseGrabber() == NULL); + stopDelayedMoveResize(); + if (QApplication::activePopupWidget() != NULL) + return false; // popups have grab + if (isFullScreen() && (screens()->count() < 2 || !isMovableAcrossScreens())) + return false; + if (!doStartMoveResize()) { + return false; + } + + invalidateDecorationDoubleClickTimer(); + + setMoveResize(true); + workspace()->setClientIsMoving(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(geometry()); // "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(geometry()); + emit quickTileModeChanged(); + } + + updateHaveResizeEffect(); + updateInitialMoveResizeGeometry(); + checkUnrestrictedMoveResize(); + emit clientStartUserMovedResized(this); + if (ScreenEdges::self()->isDesktopSwitchingMovingClients()) + ScreenEdges::self()->reserveDesktopSwitching(true, Qt::Vertical|Qt::Horizontal); + return true; +} + +bool Client::doStartMoveResize() +{ + bool has_grab = false; + // This reportedly improves smoothness of the moveresize operation, + // something with Enter/LeaveNotify events, looks like XFree performance problem or something *shrug* + // (http://lists.kde.org/?t=107302193400001&r=1&w=2) + QRect r = workspace()->clientArea(FullArea, this); + m_moveResizeGrabWindow.create(r, XCB_WINDOW_CLASS_INPUT_ONLY, 0, NULL, rootWindow()); + m_moveResizeGrabWindow.map(); + m_moveResizeGrabWindow.raise(); + updateXTime(); + const xcb_grab_pointer_cookie_t cookie = xcb_grab_pointer_unchecked(connection(), false, m_moveResizeGrabWindow, + 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, m_moveResizeGrabWindow, Cursor::x11Cursor(cursor()), xTime()); + ScopedCPointer pointerGrab(xcb_grab_pointer_reply(connection(), cookie, NULL)); + if (!pointerGrab.isNull() && pointerGrab->status == XCB_GRAB_STATUS_SUCCESS) { + has_grab = true; + } + if (!has_grab && grabXKeyboard(frameId())) + has_grab = move_resize_has_keyboard_grab = true; + if (!has_grab) { // at least one grab is necessary in order to be able to finish move/resize + m_moveResizeGrabWindow.reset(); + return false; + } + return true; +} + +void AbstractClient::finishMoveResize(bool cancel) +{ + GeometryUpdatesBlocker blocker(this); + const bool wasResize = isResize(); // store across leaveMoveResize + leaveMoveResize(); + + if (cancel) + setGeometry(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(restoreV, restoreH, false); + } + } + setGeometry(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(geometry().x()); + geom_restore.setWidth(geometry().width()); + } + if (!(maximizeMode() & MaximizeVertical)) { + geom_restore.setY(geometry().y()); + geom_restore.setHeight(geometry().height()); + } + setGeometryRestore(geom_restore); + } +// FRAME update(); + + emit clientFinishUserMovedResized(this); +} + +void Client::leaveMoveResize() +{ + if (needsXWindowMove) { + // Do the deferred move + m_frame.move(geom.topLeft()); + needsXWindowMove = false; + } + if (!isResize()) + sendSyntheticConfigureNotify(); // tell the client about it's new final position + if (geometryTip) { + geometryTip->hide(); + delete geometryTip; + geometryTip = NULL; + } + if (move_resize_has_keyboard_grab) + ungrabXKeyboard(); + move_resize_has_keyboard_grab = false; + xcb_ungrab_pointer(connection(), xTime()); + m_moveResizeGrabWindow.reset(); + if (syncRequest.counter == XCB_NONE) // don't forget to sanitize since the timeout will no more fire + syncRequest.isPending = false; + delete syncRequest.timeout; + syncRequest.timeout = NULL; + AbstractClient::leaveMoveResize(); +} + +// 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 immediatelly, +// 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]() { + 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::handleMoveResize(const QPoint &local, const QPoint &global) +{ + const QRect oldGeo = geometry(); + handleMoveResize(local.x(), local.y(), global.x(), global.y()); + if (!isFullScreen() && isMove()) { + if (quickTileMode() != QuickTileMode(QuickTileFlag::None) && oldGeo != geometry()) { + 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()); + } + } +} + +bool Client::isWaitingForMoveResizeSync() const +{ + return syncRequest.isPending && isResize(); +} + +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; + foreach (const QRect &rect, availableArea.rects()) { + 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 = adjustedSize(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()) { + 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 = adjustedSize(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; + foreach (const QRect &rect, availableArea.rects()) { + 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; + foreach (const QRect &r, strut.rects()) { + 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())); + } +} + +void Client::doResizeSync() +{ + if (!syncRequest.timeout) { + syncRequest.timeout = new QTimer(this); + connect(syncRequest.timeout, &QTimer::timeout, this, &Client::performMoveResize); + syncRequest.timeout->setSingleShot(true); + } + if (syncRequest.counter != XCB_NONE) { + syncRequest.timeout->start(250); + sendSyncRequest(); + } else { // for clients not supporting the XSYNC protocol, we + syncRequest.isPending = true; // limit the resizes to 30Hz to take pointless load from X11 + syncRequest.timeout->start(33); // and the client, the mouse is still moved at full speed + } // and no human can control faster resizes anyway + const QRect &moveResizeGeom = moveResizeGeometry(); + m_client.setGeometry(0, 0, moveResizeGeom.width() - (borderLeft() + borderRight()), moveResizeGeom.height() - (borderTop() + borderBottom())); +} + +void AbstractClient::performMoveResize() +{ + const QRect &moveResizeGeom = moveResizeGeometry(); + if (isMove() || (isResize() && !haveResizeEffect())) { + setGeometry(moveResizeGeom); + } + doPerformMoveResize(); + if (isResize()) + addRepaintFull(); + positionGeometryTip(); + emit clientStepUserMovedResized(this, moveResizeGeom); +} + +void Client::doPerformMoveResize() +{ + if (syncRequest.counter == XCB_NONE) // client w/o XSYNC support. allow the next resize event + syncRequest.isPending = false; // NEVER do this for clients with a valid counter + // (leads to sync request races in some clients) +} + +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(Cursor::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 or maximized window + if (!isResizable() && maximizeMode() != MaximizeFull) + return; + + workspace()->updateFocusMousePosition(Cursor::pos()); // may cause leave event + + GeometryUpdatesBlocker blocker(this); + + if (mode == QuickTileMode(QuickTileFlag::Maximize)) { + TabSynchronizer syncer(this, TabGroup::QuickTile|TabGroup::Geometry|TabGroup::Maximized); + 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 (geometry().top() != clientArea.top()) { + QRect r(geometry()); + r.moveTop(clientArea.top()); + setGeometry(r); + } + setGeometryRestore(prev_geom_restore); + } + 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) { + + TabSynchronizer syncer(this, TabGroup::QuickTile|TabGroup::Geometry|TabGroup::Maximized); + + 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); + + setGeometry(electricBorderMaximizeGeometry(keyboard ? geometry().center() : Cursor::pos(), desktop()), geom_mode); + // Store the mode change + m_quickTileMode = mode; + } else { + m_quickTileMode = mode; + setMaximize(false, false); + } + + emit quickTileModeChanged(); + + return; + } + + if (mode != QuickTileMode(QuickTileFlag::None)) { + TabSynchronizer syncer(this, TabGroup::QuickTile|TabGroup::Geometry); + + QPoint whichScreen = keyboard ? geometry().center() : Cursor::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 + setGeometry(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(geometry()); + } + + 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); + setGeometry(electricBorderMaximizeGeometry(whichScreen, desktop()), geom_mode); + } + + // Store the mode change + m_quickTileMode = mode; + } + + if (mode == QuickTileMode(QuickTileFlag::None)) { + TabSynchronizer syncer(this, TabGroup::QuickTile|TabGroup::Geometry); + + 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(geometry()); + // decorations may turn off some borders when tiled + const ForceGeometry_t geom_mode = isDecorated() ? ForceGeometrySet : NormalGeometrySet; + setGeometry(geometryRestore(), geom_mode); + checkWorkspacePosition(); // Just in case it's a different screen + } + emit quickTileModeChanged(); +} + +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 = geometry(); + 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); + setGeometry(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(geometry()); + + checkWorkspacePosition(oldGeom); + + // re-align geom_restore to constrained geometry + setGeometryRestore(geometry()); + + // 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); +} + +} // namespace diff --git a/geometrytip.cpp b/geometrytip.cpp new file mode 100644 index 0000000..cf826e4 --- /dev/null +++ b/geometrytip.cpp @@ -0,0 +1,66 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (c) 2003, Karol Szwed + +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, see . +*********************************************************************/ + +#include "geometrytip.h" + +namespace KWin +{ + +GeometryTip::GeometryTip(const Xcb::GeometryHints* xSizeHints): + QLabel(0) +{ + 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() +{ +} + +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 + QString pos; + pos.sprintf("%+d,%+d
(%d x %d)", + geom.x(), geom.y(), w, 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..8a8d5e3 --- /dev/null +++ b/geometrytip.h @@ -0,0 +1,44 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (c) 2003, Karol Szwed + +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, see . +*********************************************************************/ + +#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(); + 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..9d49532 --- /dev/null +++ b/gestures.cpp @@ -0,0 +1,210 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#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()); +} + +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; + // 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..ad9f893 --- /dev/null +++ b/gestures.h @@ -0,0 +1,221 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 @link{triggered} or + * @link{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 @link{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..970c79f --- /dev/null +++ b/globalshortcuts.cpp @@ -0,0 +1,312 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +// 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..b491879 --- /dev/null +++ b/globalshortcuts.h @@ -0,0 +1,192 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~GlobalShortcutsManager(); + 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 pointerButtons 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); + virtual ~InternalGlobalShortcut(); + + 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..a4c4b0a --- /dev/null +++ b/group.cpp @@ -0,0 +1,923 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ + +/* + + This file contains things relevant to window grouping. + +*/ + +//#define QT_CLEAN_NAMESPACE + +#include "group.h" +#include +#include "workspace.h" +#include "client.h" +#include "effects.h" + +#include +#include +#include +#include + + +/* + TODO + Rename as many uses of 'transient' as possible (hasTransient->hasSubwindow,etc.), + or I'll get it backwards in half of the cases again. +*/ + +namespace KWin +{ + +//******************************************** +// Group +//******************************************** + +Group::Group(Window leader_P) + : leader_client(NULL), + leader_wid(leader_P), + leader_info(NULL), + user_time(-1U), + refcount(0) +{ + if (leader_P != None) { + leader_client = workspace()->findClient(Predicate::WindowMatch, leader_P); + leader_info = new NETWinInfo(connection(), leader_P, rootWindow(), + 0, 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 != NULL) + return leader_client->icon(); + else if (leader_wid != 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(Client* member_P) +{ + _members.append(member_P); +// qDebug() << "GROUPADD:" << this << ":" << member_P; +// qDebug() << kBacktrace(); +} + +void Group::removeMember(Client* 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(Client* leader_P) +{ + assert(leader_P->window() == leader_wid); + leader_client = leader_P; +} + +void Group::lostLeader() +{ + assert(!_members.contains(leader_client)); + leader_client = NULL; + if (_members.isEmpty()) { + workspace()->removeGroup(this); + delete this; + } +} + +//*************************************** +// Workspace +//*************************************** + +Group* Workspace::findGroup(xcb_window_t leader) const +{ + assert(leader != None); + for (GroupList::ConstIterator it = groups.constBegin(); + it != groups.constEnd(); + ++it) + if ((*it)->leader() == leader) + return *it; + return NULL; +} + +// Client is group transient, but has no group set. Try to find +// group with windows with the same client leader. +Group* Workspace::findClientLeaderGroup(const Client* c) const +{ + Group* ret = NULL; + for (ClientList::ConstIterator it = clients.constBegin(); + it != clients.constEnd(); + ++it) { + if (*it == c) + continue; + if ((*it)->wmClientLeader() == c->wmClientLeader()) { + if (ret == NULL || ret == (*it)->group()) + ret = (*it)->group(); + else { + // There are already two groups with the same client leader. + // This most probably means the app uses group transients without + // setting group for its windows. Merging the two groups is a bad + // hack, but there's no really good solution for this case. + ClientList old_group = (*it)->group()->members(); + // old_group autodeletes when being empty + for (int pos = 0; + pos < old_group.count(); + ++pos) { + Client* tmp = old_group[ pos ]; + if (tmp != c) + tmp->changeClientLeaderGroup(ret); + } + } + } + } + return ret; +} + +void Workspace::updateMinimizedOfTransients(AbstractClient* c) +{ + // if mainwindow is minimized or shaded, minimize transients too + if (c->isMinimized()) { + for (auto it = c->transients().constBegin(); + it != c->transients().constEnd(); + ++it) { + if ((*it)->isModal()) + continue; // there's no reason to hide modal dialogs with the main client + // but to keep them to eg. watch progress or whatever + if (!(*it)->isMinimized()) { + (*it)->minimize(); + updateMinimizedOfTransients((*it)); + } + } + if (c->isModal()) { // if a modal dialog is minimized, minimize its mainwindow too + foreach (AbstractClient * c2, c->mainClients()) + c2->minimize(); + } + } else { + // else unmiminize the transients + for (auto it = c->transients().constBegin(); + it != c->transients().constEnd(); + ++it) { + if ((*it)->isMinimized()) { + (*it)->unminimize(); + updateMinimizedOfTransients((*it)); + } + } + if (c->isModal()) { + foreach (AbstractClient * c2, c->mainClients()) + c2->unminimize(); + } + } +} + + +/*! + Sets the client \a c's transient windows' on_all_desktops property to \a on_all_desktops. + */ +void Workspace::updateOnAllDesktopsOfTransients(AbstractClient* c) +{ + for (auto it = c->transients().constBegin(); + it != c->transients().constEnd(); + ++it) { + if ((*it)->isOnAllDesktops() != c->isOnAllDesktops()) + (*it)->setOnAllDesktops(c->isOnAllDesktops()); + } +} + +// A new window has been mapped. Check if it's not a mainwindow for some already existing transient window. +void Workspace::checkTransients(xcb_window_t w) +{ + for (ClientList::ConstIterator it = clients.constBegin(); + it != clients.constEnd(); + ++it) + (*it)->checkTransient(w); +} + + +//**************************************** +// Toplevel +//**************************************** + +// hacks for broken apps here +// all resource classes are forced to be lowercase +bool Toplevel::resourceMatch(const Toplevel* c1, const Toplevel* c2) +{ + return c1->resourceClass() == c2->resourceClass(); +} + + +//**************************************** +// Client +//**************************************** + +bool Client::belongToSameApplication(const Client* c1, const Client* c2, SameApplicationChecks checks) +{ + bool same_app = false; + + // tests that definitely mean they belong together + if (c1 == c2) + same_app = true; + else if (c1->isTransient() && c2->hasTransient(c1, true)) + same_app = true; // c1 has c2 as mainwindow + else if (c2->isTransient() && c1->hasTransient(c2, true)) + same_app = true; // c2 has c1 as mainwindow + else if (c1->group() == c2->group()) + same_app = true; // same group + else if (c1->wmClientLeader() == c2->wmClientLeader() + && c1->wmClientLeader() != c1->window() // if WM_CLIENT_LEADER is not set, it returns window(), + && c2->wmClientLeader() != c2->window()) // don't use in this test then + same_app = true; // same client leader + + // tests that mean they most probably don't belong together + else if ((c1->pid() != c2->pid() && !checks.testFlag(SameApplicationCheck::AllowCrossProcesses)) + || c1->wmClientMachine(false) != c2->wmClientMachine(false)) + ; // different processes + else if (c1->wmClientLeader() != c2->wmClientLeader() + && c1->wmClientLeader() != c1->window() // if WM_CLIENT_LEADER is not set, it returns window(), + && c2->wmClientLeader() != c2->window() // don't use in this test then + && !checks.testFlag(SameApplicationCheck::AllowCrossProcesses)) + ; // different client leader + else if (!resourceMatch(c1, c2)) + ; // different apps + else if (!sameAppWindowRoleMatch(c1, c2, checks.testFlag(SameApplicationCheck::RelaxedForActive)) + && !checks.testFlag(SameApplicationCheck::AllowCrossProcesses)) + ; // "different" apps + else if (c1->pid() == 0 || c2->pid() == 0) + ; // old apps that don't have _NET_WM_PID, consider them different + // if they weren't found to match above + else + same_app = true; // looks like it's the same app + + return same_app; +} + +// Non-transient windows with window role containing '#' are always +// considered belonging to different applications (unless +// the window role is exactly the same). KMainWindow sets +// window role this way by default, and different KMainWindow +// usually "are" different application from user's point of view. +// This help with no-focus-stealing for e.g. konqy reusing. +// On the other hand, if one of the windows is active, they are +// considered belonging to the same application. This is for +// the cases when opening new mainwindow directly from the application, +// e.g. 'Open New Window' in konqy ( active_hack == true ). +bool Client::sameAppWindowRoleMatch(const Client* c1, const Client* c2, bool active_hack) +{ + if (c1->isTransient()) { + while (const Client *t = dynamic_cast(c1->transientFor())) + c1 = t; + if (c1->groupTransient()) + return c1->group() == c2->group(); +#if 0 + // if a group transient is in its own group, it didn't possibly have a group, + // and therefore should be considered belonging to the same app like + // all other windows from the same app + || c1->group()->leaderClient() == c1 || c2->group()->leaderClient() == c2; +#endif + } + if (c2->isTransient()) { + while (const Client *t = dynamic_cast(c2->transientFor())) + c2 = t; + if (c2->groupTransient()) + return c1->group() == c2->group(); +#if 0 + || c1->group()->leaderClient() == c1 || c2->group()->leaderClient() == c2; +#endif + } + int pos1 = c1->windowRole().indexOf('#'); + int pos2 = c2->windowRole().indexOf('#'); + if ((pos1 >= 0 && pos2 >= 0)) { + if (!active_hack) // without the active hack for focus stealing prevention, + return c1 == c2; // different mainwindows are always different apps + if (!c1->isActive() && !c2->isActive()) + return c1 == c2; + else + return true; + } + return true; +} + +/* + + Transiency stuff: ICCCM 4.1.2.6, NETWM 7.3 + + WM_TRANSIENT_FOR is basically means "this is my mainwindow". + For NET::Unknown windows, transient windows are considered to be NET::Dialog + windows, for compatibility with non-NETWM clients. KWin may adjust the value + of this property in some cases (window pointing to itself or creating a loop, + keeping NET::Splash windows above other windows from the same app, etc.). + + Client::transient_for_id is the value of the WM_TRANSIENT_FOR property, after + possibly being adjusted by KWin. Client::transient_for points to the Client + this Client is transient for, or is NULL. If Client::transient_for_id is + poiting to the root window, the window is considered to be transient + for the whole window group, as suggested in NETWM 7.3. + + In the case of group transient window, Client::transient_for is NULL, + and Client::groupTransient() returns true. Such window is treated as + if it were transient for every window in its window group that has been + mapped _before_ it (or, to be exact, was added to the same group before it). + Otherwise two group transients can create loops, which can lead very very + nasty things (bug #67914 and all its dupes). + + Client::original_transient_for_id is the value of the property, which + may be different if Client::transient_for_id if e.g. forcing NET::Splash + to be kept on top of its window group, or when the mainwindow is not mapped + yet, in which case the window is temporarily made group transient, + and when the mainwindow is mapped, transiency is re-evaluated. + + This can get a bit complicated with with e.g. two Konqueror windows created + by the same process. They should ideally appear like two independent applications + to the user. This should be accomplished by all windows in the same process + having the same window group (needs to be changed in Qt at the moment), and + using non-group transients poiting to their relevant mainwindow for toolwindows + etc. KWin should handle both group and non-group transient dialogs well. + + In other words: + - non-transient windows : isTransient() == false + - normal transients : transientFor() != NULL + - group transients : groupTransient() == true + + - list of mainwindows : mainClients() (call once and loop over the result) + - list of transients : transients() + - every window in the group : group()->members() +*/ + +Xcb::TransientFor Client::fetchTransient() const +{ + return Xcb::TransientFor(window()); +} + +void Client::readTransientProperty(Xcb::TransientFor &transientFor) +{ + xcb_window_t new_transient_for_id = XCB_WINDOW_NONE; + if (transientFor.getTransientFor(&new_transient_for_id)) { + m_originalTransientForId = new_transient_for_id; + new_transient_for_id = verifyTransientFor(new_transient_for_id, true); + } else { + m_originalTransientForId = XCB_WINDOW_NONE; + new_transient_for_id = verifyTransientFor(XCB_WINDOW_NONE, false); + } + setTransient(new_transient_for_id); +} + +void Client::readTransient() +{ + Xcb::TransientFor transientFor = fetchTransient(); + readTransientProperty(transientFor); +} + +void Client::setTransient(xcb_window_t new_transient_for_id) +{ + if (new_transient_for_id != m_transientForId) { + removeFromMainClients(); + Client *transient_for = nullptr; + m_transientForId = new_transient_for_id; + if (m_transientForId != XCB_WINDOW_NONE && !groupTransient()) { + transient_for = workspace()->findClient(Predicate::WindowMatch, m_transientForId); + assert(transient_for != NULL); // verifyTransient() had to check this + transient_for->addTransient(this); + } // checkGroup() will check 'check_active_modal' + setTransientFor(transient_for); + checkGroup(NULL, true); // force, because transiency has changed + workspace()->updateClientLayer(this); + workspace()->resetUpdateToolWindowsTimer(); + emit transientChanged(); + } +} + +void Client::removeFromMainClients() +{ + if (transientFor()) + transientFor()->removeTransient(this); + if (groupTransient()) { + for (ClientList::ConstIterator it = group()->members().constBegin(); + it != group()->members().constEnd(); + ++it) + (*it)->removeTransient(this); + } +} + +// *sigh* this transiency handling is madness :( +// This one is called when destroying/releasing a window. +// It makes sure this client is removed from all grouping +// related lists. +void Client::cleanGrouping() +{ +// qDebug() << "CLEANGROUPING:" << this; +// for ( ClientList::ConstIterator it = group()->members().begin(); +// it != group()->members().end(); +// ++it ) +// qDebug() << "CL:" << *it; +// ClientList mains; +// mains = mainClients(); +// for ( ClientList::ConstIterator it = mains.begin(); +// it != mains.end(); +// ++it ) +// qDebug() << "MN:" << *it; + removeFromMainClients(); +// qDebug() << "CLEANGROUPING2:" << this; +// for ( ClientList::ConstIterator it = group()->members().begin(); +// it != group()->members().end(); +// ++it ) +// qDebug() << "CL2:" << *it; +// mains = mainClients(); +// for ( ClientList::ConstIterator it = mains.begin(); +// it != mains.end(); +// ++it ) +// qDebug() << "MN2:" << *it; + for (auto it = transients().constBegin(); + it != transients().constEnd(); + ) { + if ((*it)->transientFor() == this) { + removeTransient(*it); + it = transients().constBegin(); // restart, just in case something more has changed with the list + } else + ++it; + } +// qDebug() << "CLEANGROUPING3:" << this; +// for ( ClientList::ConstIterator it = group()->members().begin(); +// it != group()->members().end(); +// ++it ) +// qDebug() << "CL3:" << *it; +// mains = mainClients(); +// for ( ClientList::ConstIterator it = mains.begin(); +// it != mains.end(); +// ++it ) +// qDebug() << "MN3:" << *it; + // HACK + // removeFromMainClients() did remove 'this' from transient + // lists of all group members, but then made windows that + // were transient for 'this' group transient, which again + // added 'this' to those transient lists :( + ClientList group_members = group()->members(); + group()->removeMember(this); + in_group = NULL; + for (ClientList::ConstIterator it = group_members.constBegin(); + it != group_members.constEnd(); + ++it) + (*it)->removeTransient(this); +// qDebug() << "CLEANGROUPING4:" << this; +// for ( ClientList::ConstIterator it = group_members.begin(); +// it != group_members.end(); +// ++it ) +// qDebug() << "CL4:" << *it; + m_transientForId = XCB_WINDOW_NONE; +} + +// Make sure that no group transient is considered transient +// for a window that is (directly or indirectly) transient for it +// (including another group transients). +// Non-group transients not causing loops are checked in verifyTransientFor(). +void Client::checkGroupTransients() +{ + for (ClientList::ConstIterator it1 = group()->members().constBegin(); + it1 != group()->members().constEnd(); + ++it1) { + if (!(*it1)->groupTransient()) // check all group transients in the group + continue; // TODO optimize to check only the changed ones? + for (ClientList::ConstIterator it2 = group()->members().constBegin(); + it2 != group()->members().constEnd(); + ++it2) { // group transients can be transient only for others in the group, + // so don't make them transient for the ones that are transient for it + if (*it1 == *it2) + continue; + for (AbstractClient* cl = (*it2)->transientFor(); + cl != NULL; + cl = cl->transientFor()) { + if (cl == *it1) { + // don't use removeTransient(), that would modify *it2 too + (*it2)->removeTransientFromList(*it1); + continue; + } + } + // if *it1 and *it2 are both group transients, and are transient for each other, + // make only *it2 transient for *it1 (i.e. subwindow), as *it2 came later, + // and should be therefore on top of *it1 + // TODO This could possibly be optimized, it also requires hasTransient() to check for loops. + if ((*it2)->groupTransient() && (*it1)->hasTransient(*it2, true) && (*it2)->hasTransient(*it1, true)) + (*it2)->removeTransientFromList(*it1); + // if there are already windows W1 and W2, W2 being transient for W1, and group transient W3 + // is added, make it transient only for W2, not for W1, because it's already indirectly + // transient for it - the indirect transiency actually shouldn't break anything, + // but it can lead to exponentially expensive operations (#95231) + // TODO this is pretty slow as well + for (ClientList::ConstIterator it3 = group()->members().constBegin(); + it3 != group()->members().constEnd(); + ++it3) { + if (*it1 == *it2 || *it2 == *it3 || *it1 == *it3) + continue; + if ((*it2)->hasTransient(*it1, false) && (*it3)->hasTransient(*it1, false)) { + if ((*it2)->hasTransient(*it3, true)) + (*it2)->removeTransientFromList(*it1); + if ((*it3)->hasTransient(*it2, true)) + (*it3)->removeTransientFromList(*it1); + } + } + } + } +} + +/*! + Check that the window is not transient for itself, and similar nonsense. + */ +xcb_window_t Client::verifyTransientFor(xcb_window_t new_transient_for, bool set) +{ + xcb_window_t new_property_value = new_transient_for; + // make sure splashscreens are shown above all their app's windows, even though + // they're in Normal layer + if (isSplash() && new_transient_for == XCB_WINDOW_NONE) + new_transient_for = rootWindow(); + if (new_transient_for == XCB_WINDOW_NONE) { + if (set) // sometimes WM_TRANSIENT_FOR is set to None, instead of root window + new_property_value = new_transient_for = rootWindow(); + else + return XCB_WINDOW_NONE; + } + if (new_transient_for == window()) { // pointing to self + // also fix the property itself + qCWarning(KWIN_CORE) << "Client " << this << " has WM_TRANSIENT_FOR poiting to itself." ; + new_property_value = new_transient_for = rootWindow(); + } +// The transient_for window may be embedded in another application, +// so kwin cannot see it. Try to find the managed client for the +// window and fix the transient_for property if possible. + xcb_window_t before_search = new_transient_for; + while (new_transient_for != XCB_WINDOW_NONE + && new_transient_for != rootWindow() + && !workspace()->findClient(Predicate::WindowMatch, new_transient_for)) { + Xcb::Tree tree(new_transient_for); + if (tree.isNull()) { + break; + } + new_transient_for = tree->parent; + } + if (Client* new_transient_for_client = workspace()->findClient(Predicate::WindowMatch, new_transient_for)) { + if (new_transient_for != before_search) { + qCDebug(KWIN_CORE) << "Client " << this << " has WM_TRANSIENT_FOR poiting to non-toplevel window " + << before_search << ", child of " << new_transient_for_client << ", adjusting."; + new_property_value = new_transient_for; // also fix the property + } + } else + new_transient_for = before_search; // nice try +// loop detection +// group transients cannot cause loops, because they're considered transient only for non-transient +// windows in the group + int count = 20; + xcb_window_t loop_pos = new_transient_for; + while (loop_pos != XCB_WINDOW_NONE && loop_pos != rootWindow()) { + Client* pos = workspace()->findClient(Predicate::WindowMatch, loop_pos); + if (pos == NULL) + break; + loop_pos = pos->m_transientForId; + if (--count == 0 || pos == this) { + qCWarning(KWIN_CORE) << "Client " << this << " caused WM_TRANSIENT_FOR loop." ; + new_transient_for = rootWindow(); + } + } + if (new_transient_for != rootWindow() + && workspace()->findClient(Predicate::WindowMatch, new_transient_for) == NULL) { + // it's transient for a specific window, but that window is not mapped + new_transient_for = rootWindow(); + } + if (new_property_value != m_originalTransientForId) + Xcb::setTransientFor(window(), new_property_value); + return new_transient_for; +} + +void Client::addTransient(AbstractClient* cl) +{ + AbstractClient::addTransient(cl); + if (workspace()->mostRecentlyActivatedClient() == this && cl->isModal()) + check_active_modal = true; +// qDebug() << "ADDTRANS:" << this << ":" << cl; +// qDebug() << kBacktrace(); +// for ( ClientList::ConstIterator it = transients_list.begin(); +// it != transients_list.end(); +// ++it ) +// qDebug() << "AT:" << (*it); +} + +void Client::removeTransient(AbstractClient* cl) +{ +// qDebug() << "REMOVETRANS:" << this << ":" << cl; +// qDebug() << kBacktrace(); + // cl is transient for this, but this is going away + // make cl group transient + AbstractClient::removeTransient(cl); + if (cl->transientFor() == this) { + if (Client *c = dynamic_cast(cl)) { + c->m_transientForId = XCB_WINDOW_NONE; + c->setTransientFor(nullptr); // SELI +// SELI cl->setTransient( rootWindow()); + c->setTransient(XCB_WINDOW_NONE); + } + } +} + +// A new window has been mapped. Check if it's not a mainwindow for this already existing window. +void Client::checkTransient(xcb_window_t w) +{ + if (m_originalTransientForId != w) + return; + w = verifyTransientFor(w, true); + setTransient(w); +} + +// returns true if cl is the transient_for window for this client, +// or recursively the transient_for window +bool Client::hasTransient(const AbstractClient* cl, bool indirect) const +{ + if (const Client *c = dynamic_cast(cl)) { + // checkGroupTransients() uses this to break loops, so hasTransient() must detect them + ConstClientList set; + return hasTransientInternal(c, indirect, set); + } + return false; +} + +bool Client::hasTransientInternal(const Client* cl, bool indirect, ConstClientList& set) const +{ + if (const Client *t = dynamic_cast(cl->transientFor())) { + if (t == this) + return true; + if (!indirect) + return false; + if (set.contains(cl)) + return false; + set.append(cl); + return hasTransientInternal(t, indirect, set); + } + if (!cl->isTransient()) + return false; + if (group() != cl->group()) + return false; + // cl is group transient, search from top + if (transients().contains(const_cast< Client* >(cl))) + return true; + if (!indirect) + return false; + if (set.contains(this)) + return false; + set.append(this); + for (auto it = transients().constBegin(); + it != transients().constEnd(); + ++it) { + const Client *c = qobject_cast(*it); + if (!c) { + continue; + } + if (c->hasTransientInternal(cl, indirect, set)) + return true; + } + return false; +} + +QList Client::mainClients() const +{ + if (!isTransient()) + return QList(); + if (const AbstractClient *t = transientFor()) + return QList{const_cast< AbstractClient* >(t)}; + QList result; + Q_ASSERT(group()); + for (ClientList::ConstIterator it = group()->members().constBegin(); + it != group()->members().constEnd(); + ++it) + if ((*it)->hasTransient(this, false)) + result.append(*it); + return result; +} + +AbstractClient* Client::findModal(bool allow_itself) +{ + for (auto it = transients().constBegin(); + it != transients().constEnd(); + ++it) + if (AbstractClient* ret = (*it)->findModal(true)) + return ret; + if (isModal() && allow_itself) + return this; + return NULL; +} + +// Client::window_group only holds the contents of the hint, +// but it should be used only to find the group, not for anything else +// Argument is only when some specific group needs to be set. +void Client::checkGroup(Group* set_group, bool force) +{ + Group* old_group = in_group; + if (old_group != NULL) + old_group->ref(); // turn off automatic deleting + if (set_group != NULL) { + if (set_group != in_group) { + if (in_group != NULL) + in_group->removeMember(this); + in_group = set_group; + in_group->addMember(this); + } + } else if (info->groupLeader() != XCB_WINDOW_NONE) { + Group* new_group = workspace()->findGroup(info->groupLeader()); + Client *t = qobject_cast(transientFor()); + if (t != NULL && t->group() != new_group) { + // move the window to the right group (e.g. a dialog provided + // by different app, but transient for this one, so make it part of that group) + new_group = t->group(); + } + if (new_group == NULL) // doesn't exist yet + new_group = new Group(info->groupLeader()); + if (new_group != in_group) { + if (in_group != NULL) + in_group->removeMember(this); + in_group = new_group; + in_group->addMember(this); + } + } else { + if (Client *t = qobject_cast(transientFor())) { + // doesn't have window group set, but is transient for something + // so make it part of that group + Group* new_group = t->group(); + if (new_group != in_group) { + if (in_group != NULL) + in_group->removeMember(this); + in_group = t->group(); + in_group->addMember(this); + } + } else if (groupTransient()) { + // group transient which actually doesn't have a group :( + // try creating group with other windows with the same client leader + Group* new_group = workspace()->findClientLeaderGroup(this); + if (new_group == NULL) + new_group = new Group(None); + if (new_group != in_group) { + if (in_group != NULL) + in_group->removeMember(this); + in_group = new_group; + in_group->addMember(this); + } + } else { // Not transient without a group, put it in its client leader group. + // This might be stupid if grouping was used for e.g. taskbar grouping + // or minimizing together the whole group, but as long as it is used + // only for dialogs it's better to keep windows from one app in one group. + Group* new_group = workspace()->findClientLeaderGroup(this); + if (in_group != NULL && in_group != new_group) { + in_group->removeMember(this); + in_group = NULL; + } + if (new_group == NULL) + new_group = new Group(None); + if (in_group != new_group) { + in_group = new_group; + in_group->addMember(this); + } + } + } + if (in_group != old_group || force) { + for (auto it = transients().constBegin(); + it != transients().constEnd(); + ) { + Client *c = dynamic_cast(*it); + if (!c) { + ++it; + continue; + } + // group transients in the old group are no longer transient for it + if (c->groupTransient() && c->group() != group()) { + removeTransientFromList(c); + it = transients().constBegin(); // restart, just in case something more has changed with the list + } else + ++it; + } + if (groupTransient()) { + // no longer transient for ones in the old group + if (old_group != NULL) { + for (ClientList::ConstIterator it = old_group->members().constBegin(); + it != old_group->members().constEnd(); + ++it) + (*it)->removeTransient(this); + } + // and make transient for all in the new group + for (ClientList::ConstIterator it = group()->members().constBegin(); + it != group()->members().constEnd(); + ++it) { + if (*it == this) + break; // this means the window is only transient for windows mapped before it + (*it)->addTransient(this); + } + } + // group transient splashscreens should be transient even for windows + // in group mapped later + for (ClientList::ConstIterator it = group()->members().constBegin(); + it != group()->members().constEnd(); + ++it) { + if (!(*it)->isSplash()) + continue; + if (!(*it)->groupTransient()) + continue; + if (*it == this || hasTransient(*it, true)) // TODO indirect? + continue; + addTransient(*it); + } + } + if (old_group != NULL) + old_group->deref(); // can be now deleted if empty + checkGroupTransients(); + checkActiveModal(); + workspace()->updateClientLayer(this); +} + +// used by Workspace::findClientLeaderGroup() +void Client::changeClientLeaderGroup(Group* gr) +{ + // transientFor() != NULL are in the group of their mainwindow, so keep them there + if (transientFor() != NULL) + return; + // also don't change the group for window which have group set + if (info->groupLeader()) + return; + checkGroup(gr); // change group +} + +bool Client::check_active_modal = false; + +void Client::checkActiveModal() +{ + // if the active window got new modal transient, activate it. + // cannot be done in AddTransient(), because there may temporarily + // exist loops, breaking findModal + Client* check_modal = dynamic_cast(workspace()->mostRecentlyActivatedClient()); + if (check_modal != NULL && check_modal->check_active_modal) { + Client* new_modal = dynamic_cast(check_modal->findModal()); + if (new_modal != NULL && new_modal != check_modal) { + if (!new_modal->isManaged()) + return; // postpone check until end of manage() + workspace()->activateClient(new_modal); + } + check_modal->check_active_modal = false; + } +} + +} // namespace diff --git a/group.h b/group.h new file mode 100644 index 0000000..f073969 --- /dev/null +++ b/group.h @@ -0,0 +1,99 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ + +#ifndef KWIN_GROUP_H +#define KWIN_GROUP_H + +#include "utils.h" +#include +#include +#include + +namespace KWin +{ + +class Client; +class EffectWindowGroupImpl; + +class Group +{ +public: + Group(Window leader); + ~Group(); + Window leader() const; + const Client* leaderClient() const; + Client* leaderClient(); + const ClientList& members() const; + QIcon icon() const; + void addMember(Client* member); + void removeMember(Client* member); + void gotLeader(Client* leader); + void lostLeader(); + void updateUserTime(xcb_timestamp_t time); + xcb_timestamp_t userTime() const; + void ref(); + void deref(); + EffectWindowGroupImpl* effectGroup(); +private: + void startupIdChanged(); + ClientList _members; + Client* leader_client; + Window leader_wid; + NETWinInfo* leader_info; + xcb_timestamp_t user_time; + int refcount; + EffectWindowGroupImpl* effect_group; +}; + +inline Window Group::leader() const +{ + return leader_wid; +} + +inline const Client* Group::leaderClient() const +{ + return leader_client; +} + +inline Client* Group::leaderClient() +{ + return leader_client; +} + +inline const ClientList& 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..fcd7fda --- /dev/null +++ b/helpers/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(killer) +add_subdirectory(xclipboardsync) diff --git a/helpers/killer/CMakeLists.txt b/helpers/killer/CMakeLists.txt new file mode 100644 index 0000000..9e867ec --- /dev/null +++ b/helpers/killer/CMakeLists.txt @@ -0,0 +1,16 @@ +########### next target ############### + +set(kwin_killer_helper_SRCS killer.cpp ) + + +add_executable(kwin_killer_helper ${kwin_killer_helper_SRCS}) + +target_link_libraries(kwin_killer_helper + Qt5::Widgets + Qt5::X11Extras + KF5::Auth + KF5::I18n + KF5::WidgetsAddons +) + +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..1dd25ea --- /dev/null +++ b/helpers/killer/killer.cpp @@ -0,0 +1,131 @@ +/**************************************************************************** + + Copyright (C) 2003 Lubos Lunak + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. + +****************************************************************************/ + +#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); + 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/helpers/xclipboardsync/CMakeLists.txt b/helpers/xclipboardsync/CMakeLists.txt new file mode 100644 index 0000000..8b4ea9a --- /dev/null +++ b/helpers/xclipboardsync/CMakeLists.txt @@ -0,0 +1,5 @@ +set(xclipboard_SRCS main.cpp waylandclipboard.cpp) +add_executable(org_kde_kwin_xclipboard_syncer ${xclipboard_SRCS}) +target_link_libraries(org_kde_kwin_xclipboard_syncer Qt5::Gui KF5::WaylandClient KF5::Crash) + +install(TARGETS org_kde_kwin_xclipboard_syncer DESTINATION ${LIBEXEC_INSTALL_DIR} ) diff --git a/helpers/xclipboardsync/main.cpp b/helpers/xclipboardsync/main.cpp new file mode 100644 index 0000000..1a5e5ee --- /dev/null +++ b/helpers/xclipboardsync/main.cpp @@ -0,0 +1,47 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "waylandclipboard.h" + +#include +#include + +#include +#if HAVE_PR_SET_PDEATHSIG +#include +#include +#endif + +int main(int argc, char *argv[]) +{ +#if HAVE_PR_SET_PDEATHSIG + prctl(PR_SET_PDEATHSIG, SIGTERM); +#endif + qputenv("QT_QPA_PLATFORM", "xcb"); + QGuiApplication app(argc, argv); + // perform sanity checks + if (app.platformName().toLower() != QStringLiteral("xcb")) { + fprintf(stderr, "%s: FATAL ERROR expecting platform xcb but got platform %s\n", + argv[0], qPrintable(app.platformName())); + return 1; + } + KCrash::initialize(); + new WaylandClipboard(&app); + return app.exec(); +} diff --git a/helpers/xclipboardsync/waylandclipboard.cpp b/helpers/xclipboardsync/waylandclipboard.cpp new file mode 100644 index 0000000..2362304 --- /dev/null +++ b/helpers/xclipboardsync/waylandclipboard.cpp @@ -0,0 +1,176 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "waylandclipboard.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +using namespace KWayland::Client; + +WaylandClipboard::WaylandClipboard(QObject *parent) + : QObject(parent) + , m_thread(new QThread) + , m_connectionThread(new ConnectionThread) +{ + m_connectionThread->setSocketFd(qgetenv("WAYLAND_SOCKET").toInt()); + m_connectionThread->moveToThread(m_thread); + m_thread->start(); + + connect(m_connectionThread, &ConnectionThread::connected, this, &WaylandClipboard::setup, Qt::QueuedConnection); + + m_connectionThread->initConnection(); + + connect(qApp->clipboard(), &QClipboard::changed, this, + [this] (QClipboard::Mode mode) { + if (mode != QClipboard::Clipboard) { + return; + } + // TODO: do we need to take a copy of the clipboard in order to keep it after the X application quit? + if (!m_dataDeviceManager || !m_dataDevice) { + return; + } + auto source = m_dataDeviceManager->createDataSource(this); + auto mimeData = qApp->clipboard()->mimeData(); + const auto formats = mimeData->formats(); + for (const auto &format : formats) { + source->offer(format); + } + connect(source, &DataSource::sendDataRequested, this, + [] (const QString &type, qint32 fd) { + auto mimeData = qApp->clipboard()->mimeData(); + if (!mimeData->hasFormat(type)) { + close(fd); + return; + } + const auto data = mimeData->data(type); + QFile writePipe; + if (writePipe.open(fd, QIODevice::WriteOnly, QFile::AutoCloseHandle)) { + writePipe.write(data); + writePipe.close(); + } else { + close(fd); + } + } + ); + m_dataDevice->setSelection(0, source); + delete m_dataSource; + m_dataSource = source; + m_connectionThread->flush(); + } + ); +} + +WaylandClipboard::~WaylandClipboard() +{ + m_connectionThread->deleteLater(); + m_thread->quit(); + m_thread->wait(); +} + +static int readData(int fd, QByteArray &data) +{ + // implementation based on QtWayland file qwaylanddataoffer.cpp + char buf[4096]; + int retryCount = 0; + int n; + while (true) { + n = QT_READ(fd, buf, sizeof buf); + if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK) && ++retryCount < 1000) { + usleep(1000); + } else { + break; + } + } + if (n > 0) { + data.append(buf, n); + n = readData(fd, data); + } + return n; +} + +void WaylandClipboard::setup() +{ + EventQueue *queue = new EventQueue(this); + queue->setup(m_connectionThread); + + Registry *registry = new Registry(this); + registry->setEventQueue(queue); + registry->create(m_connectionThread); + connect(registry, &Registry::interfacesAnnounced, this, + [this, registry] { + const auto seatInterface = registry->interface(Registry::Interface::Seat); + if (seatInterface.name != 0) { + m_seat = registry->createSeat(seatInterface.name, seatInterface.version, this); + } + const auto ddmInterface = registry->interface(Registry::Interface::DataDeviceManager); + if (ddmInterface.name != 0) { + m_dataDeviceManager = registry->createDataDeviceManager(ddmInterface.name, ddmInterface.version, this); + } + if (m_seat && m_dataDeviceManager) { + m_dataDevice = m_dataDeviceManager->getDataDevice(m_seat, this); + connect(m_dataDevice, &DataDevice::selectionOffered, this, + [this] (DataOffer *offer) { + if (offer->offeredMimeTypes().isEmpty()) { + return; + } + int pipeFds[2]; + if (pipe(pipeFds) != 0) { + return; + } + const auto mimeType = offer->offeredMimeTypes().first(); + offer->receive(mimeType, pipeFds[1]); + m_connectionThread->flush(); + close(pipeFds[1]); + QByteArray content; + if (readData(pipeFds[0], content) != 0) { + content = QByteArray(); + } + close(pipeFds[0]); + QMimeData *mimeData = new QMimeData(); + mimeData->setData(mimeType.name(), content); + qApp->clipboard()->setMimeData(mimeData); + } + ); + connect(m_dataDevice, &DataDevice::selectionCleared, this, + [this] { + qApp->clipboard()->clear(); + } + ); + } + } + ); + registry->setup(); +} diff --git a/helpers/xclipboardsync/waylandclipboard.h b/helpers/xclipboardsync/waylandclipboard.h new file mode 100644 index 0000000..2009959 --- /dev/null +++ b/helpers/xclipboardsync/waylandclipboard.h @@ -0,0 +1,56 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef WAYLANDCLIPBOARD_H +#define WAYLANDCLIPBOARD_H + +#include + +class QThread; + +namespace KWayland +{ +namespace Client +{ +class ConnectionThread; +class Seat; +class DataDeviceManager; +class DataDevice; +class DataSource; +} +} + +class WaylandClipboard : public QObject +{ + Q_OBJECT +public: + explicit WaylandClipboard(QObject *parent); + ~WaylandClipboard(); + +private: + void setup(); + QThread *m_thread; + KWayland::Client::ConnectionThread *m_connectionThread; + KWayland::Client::Seat *m_seat = nullptr; + KWayland::Client::DataDeviceManager *m_dataDeviceManager = nullptr; + KWayland::Client::DataDevice *m_dataDevice = nullptr; + KWayland::Client::DataSource *m_dataSource = nullptr; +}; + +#endif diff --git a/idle_inhibition.cpp b/idle_inhibition.cpp new file mode 100644 index 0000000..6e78b40 --- /dev/null +++ b/idle_inhibition.cpp @@ -0,0 +1,89 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#include "idle_inhibition.h" +#include "deleted.h" +#include "shell_client.h" + +#include +#include + +#include + +using KWayland::Server::SurfaceInterface; + +namespace KWin +{ + +IdleInhibition::IdleInhibition(IdleInterface *idle) + : QObject(idle) + , m_idle(idle) +{ +} + +IdleInhibition::~IdleInhibition() = default; + +void IdleInhibition::registerShellClient(ShellClient *client) +{ + auto surface = client->surface(); + m_connections.insert(client, connect(surface, &SurfaceInterface::inhibitsIdleChanged, this, + [this, client] { + // TODO: only inhibit if the ShellClient is visible + if (client->surface()->inhibitsIdle()) { + inhibit(client); + } else { + uninhibit(client); + } + } + )); + connect(client, &ShellClient::windowClosed, this, + [this, client] { + uninhibit(client); + auto it = m_connections.find(client); + if (it != m_connections.end()) { + disconnect(it.value()); + m_connections.erase(it); + } + } + ); +} + +void IdleInhibition::inhibit(ShellClient *client) +{ + if (isInhibited(client)) { + // already inhibited + return; + } + m_idleInhibitors << client; + m_idle->inhibit(); + // TODO: notify powerdevil? +} + +void IdleInhibition::uninhibit(ShellClient *client) +{ + auto it = std::find_if(m_idleInhibitors.begin(), m_idleInhibitors.end(), [client] (auto c) { return c == client; }); + if (it == m_idleInhibitors.end()) { + // not inhibited + return; + } + m_idleInhibitors.erase(it); + m_idle->uninhibit(); +} + +} diff --git a/idle_inhibition.h b/idle_inhibition.h new file mode 100644 index 0000000..1c90119 --- /dev/null +++ b/idle_inhibition.h @@ -0,0 +1,66 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#pragma once + +#include +#include +#include + +#include + +namespace KWayland +{ +namespace Server +{ +class IdleInterface; +} +} + +using KWayland::Server::IdleInterface; + +namespace KWin +{ +class ShellClient; + +class IdleInhibition : public QObject +{ + Q_OBJECT +public: + explicit IdleInhibition(IdleInterface *idle); + ~IdleInhibition(); + + void registerShellClient(ShellClient *client); + + bool isInhibited() const { + return !m_idleInhibitors.isEmpty(); + } + bool isInhibited(ShellClient *client) const { + return std::any_of(m_idleInhibitors.begin(), m_idleInhibitors.end(), [client] (auto c) { return c == client; }); + } + +private: + void inhibit(ShellClient *client); + void uninhibit(ShellClient *client); + + IdleInterface *m_idle; + QVector m_idleInhibitors; + QMap m_connections; +}; +} diff --git a/input.cpp b/input.cpp new file mode 100644 index 0000000..f7564d5 --- /dev/null +++ b/input.cpp @@ -0,0 +1,2188 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "input.h" +#include "input_event.h" +#include "input_event_spy.h" +#include "keyboard_input.h" +#include "pointer_input.h" +#include "touch_input.h" +#include "client.h" +#include "effects.h" +#include "gestures.h" +#include "globalshortcuts.h" +#include "logind.h" +#include "main.h" +#ifdef KWIN_BUILD_TABBOX +#include "tabbox/tabbox.h" +#endif +#include "unmanaged.h" +#include "screenedge.h" +#include "screens.h" +#include "workspace.h" +#include "libinput/connection.h" +#include "libinput/device.h" +#include "platform.h" +#include "popup_input_filter.h" +#include "shell_client.h" +#include "wayland_server.h" +#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(quint32 id, const QPointF &point, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(point) + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::touchMotion(quint32 id, const QPointF &point, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(point) + Q_UNUSED(time) + return false; +} + +bool InputEventFilter::touchUp(quint32 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; +} + +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) { + input()->pointer()->update(); + if (pointerSurfaceAllowed()) { + seat->setPointerPos(event->screenPos().toPoint()); + } + } else if (event->type() == QEvent::MouseButtonPress || event->type() == QEvent::MouseButtonRelease) { + if (pointerSurfaceAllowed()) { + 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(quint32 id, const QPointF &pos, quint32 time) override { + if (!waylandServer()->isScreenLocked()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + if (!seat->isTouchSequence()) { + input()->touch()->update(pos); + } + if (touchSurfaceAllowed()) { + input()->touch()->insertId(id, seat->touchDown(pos)); + } + return true; + } + bool touchMotion(quint32 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(quint32 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(KWayland::Server::SurfaceInterface *(KWayland::Server::SeatInterface::*method)() const) const { + if (KWayland::Server::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(&KWayland::Server::SeatInterface::focusedPointerSurface); + } + bool keyboardSurfaceAllowed() const { + return surfaceAllowed(&KWayland::Server::SeatInterface::focusedKeyboardSurface); + } + bool touchSurfaceAllowed() const { + return surfaceAllowed(&KWayland::Server::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 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(quint32 id, const QPointF &pos, quint32 time) override { + if (!effects) { + return false; + } + return static_cast< EffectsHandlerImpl* >(effects)->touchDown(id, pos, time); + } + bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override { + if (!effects) { + return false; + } + return static_cast< EffectsHandlerImpl* >(effects)->touchMotion(id, pos, time); + } + bool touchUp(quint32 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()->getMovingClient(); + 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()->getMovingClient() != nullptr; + } + bool keyEvent(QKeyEvent *event) override { + AbstractClient *c = workspace()->getMovingClient(); + 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; + } +}; + +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(quint32 id, const QPointF &pos, quint32 time) override { + Q_UNUSED(time) + if (!isActive()) { + return false; + } + m_touchPoints.insert(id, pos); + return true; + } + + bool touchMotion(quint32 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(quint32 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: + 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->type() == QEvent::KeyPress) { + 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; + } +}; + + +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()) { + 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()) { + 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; + } + if (event->buttons() == Qt::NoButton) { + // update pointer window only if no button is pressed + input()->pointer()->update(); + } + if (!internal) { + return false; + } + // find client + switch (event->type()) + { + case QEvent::MouseButtonPress: + case QEvent::MouseButtonRelease: { + auto s = waylandServer()->findClient(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 = waylandServer()->findClient(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 auto &internalClients = waylandServer()->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(quint32 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 a decoration, ignore further touch points, but filter out + return true; + } + // a new touch point + seat->setTimestamp(time); + touch->update(pos); + 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()); + 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(quint32 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(quint32 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); + + 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: { + if (event->buttons() == Qt::NoButton) { + return false; + } + 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(quint32 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); + input()->touch()->update(pos); + auto decoration = input()->touch()->decoration(); + if (!decoration) { + return false; + } + input()->touch()->setDecorationPressId(id); + m_lastGlobalTouchPos = pos; + m_lastLocalTouchPos = pos - decoration->client()->pos(); + 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(quint32 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(); + if (auto c = workspace()->getMovingClient()) { + c->updateMoveResize(pos); + } else { + 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(quint32 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 + if (auto c = workspace()->getMovingClient()) { + c->endMoveResize(); + } else { + 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); + if (input()->pointer()->decoration() == decoration) { + // send motion to current pointer position + const QPointF p = input()->pointer()->pos() - decoration->client()->pos(); + QHoverEvent event(QEvent::HoverMove, p, p); + QCoreApplication::instance()->sendEvent(decoration->decoration(), &event); + } else { + // send leave + QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); + QCoreApplication::instance()->sendEvent(decoration->decoration(), &event); + } + } + + 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(quint32 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(quint32 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(quint32 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; + quint32 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()->window().data()); + 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()->window().data()); + if (!c) { + return false; + } + const auto actionResult = performClientWheelAction(event, c, MouseAction::ModifierAndWindow); + if (actionResult.first) { + return actionResult.second; + } + return false; + } + bool touchDown(quint32 id, const QPointF &pos, quint32 time) override { + Q_UNUSED(id) + Q_UNUSED(time) + auto seat = waylandServer()->seat(); + if (seat->isTouchSequence()) { + return false; + } + input()->touch()->update(pos); + AbstractClient *c = dynamic_cast(input()->touch()->window().data()); + 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: { + if (event->buttons() == Qt::NoButton) { + // update pointer window only if no button is pressed + input()->pointer()->update(); + input()->pointer()->updatePointerConstraints(); + } + 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); + if (event->buttons() == Qt::NoButton) { + input()->pointer()->update(); + } + break; + default: + break; + } + return true; + } + bool wheelEvent(QWheelEvent *event) override { + auto seat = waylandServer()->seat(); + 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 (!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(quint32 id, const QPointF &pos, quint32 time) override { + if (!workspace()) { + return false; + } + auto seat = waylandServer()->seat(); + seat->setTimestamp(time); + if (!seat->isTouchSequence()) { + input()->touch()->update(pos); + } + input()->touch()->insertId(id, seat->touchDown(pos)); + return true; + } + bool touchMotion(quint32 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(quint32 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; + } +}; + +class DragAndDropInputFilter : public InputEventFilter +{ +public: + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override { + auto seat = waylandServer()->seat(); + if (!seat->isDragPointer()) { + return false; + } + seat->setTimestamp(event->timestamp()); + switch (event->type()) { + case QEvent::MouseMove: { + if (Toplevel *t = input()->findToplevel(event->globalPos())) { + // TODO: consider decorations + if (t->surface() != seat->dragSurface()) { + if (AbstractClient *c = qobject_cast(t)) { + workspace()->activateClient(c); + } + seat->setPointerPos(event->globalPos()); + seat->setDragTarget(t->surface(), event->globalPos(), t->inputTransformation()); + } + } else { + // no window at that place, if we have a surface we need to reset + seat->setDragTarget(nullptr); + } + seat->setPointerPos(event->globalPos()); + 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; + } +}; + +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_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 = NULL; + 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 KWayland::Server; + FakeInputInterface *fakeInput = waylandServer()->display()->createFakeInput(this); + fakeInput->create(); + connect(fakeInput, &FakeInputInterface::deviceCreated, this, + [this] (FakeInputDevice *device) { + connect(device, &FakeInputDevice::authenticationRequested, this, + [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::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); + waylandServer()->simulateUserActivity(); + } + ); + connect(device, &FakeInputDevice::touchDownRequested, this, + [this] (quint32 id, const QPointF &pos) { + // TODO: Fix time + m_touch->processDown(id, pos, 0); + waylandServer()->simulateUserActivity(); + } + ); + connect(device, &FakeInputDevice::touchMotionRequested, this, + [this] (quint32 id, const QPointF &pos) { + // TODO: Fix time + m_touch->processMotion(id, pos, 0); + waylandServer()->simulateUserActivity(); + } + ); + connect(device, &FakeInputDevice::touchUpRequested, this, + [this] (quint32 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(workspace(), &Workspace::configChanged, this, &InputRedirection::reconfigure); + + m_keyboard->init(); + m_pointer->init(); + m_touch->init(); + } + setupInputFilters(); +} + +void InputRedirection::setupInputFilters() +{ + if (LogindIntegration::self()->hasSessionControl()) { + installInputEventFilter(new VirtualTerminalFilter); + } + if (waylandServer()) { + installInputEventFilter(new TerminateServerFilter); + installInputEventFilter(new DragAndDropInputFilter); + installInputEventFilter(new LockScreenFilter); + installInputEventFilter(new PopupInputFilter); + m_windowSelector = new WindowSelectorFilter; + installInputEventFilter(m_windowSelector); + } + installInputEventFilter(new ScreenEdgeInputFilter); + installInputEventFilter(new EffectsFilter); + installInputEventFilter(new MoveResizeFilter); +#ifdef KWIN_BUILD_TABBOX + installInputEventFilter(new TabBoxInputFilter); +#endif + installInputEventFilter(new GlobalShortcutFilter); + installInputEventFilter(new InternalWindowEventFilter); + installInputEventFilter(new DecorationEventFilter); + if (waylandServer()) { + installInputEventFilter(new WindowActionInputFilter); + installInputEventFilter(new ForwardInputFilter); + } +} + +void InputRedirection::reconfigure() +{ + if (Application::usesLibinput()) { + auto inputConfig = kwinApp()->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); + } +} + +static KWayland::Server::SeatInterface *findSeat() +{ + auto server = waylandServer(); + if (!server) { + return nullptr; + } + return server->seat(); +} + +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(KWayland::Server::RelativePointerInterfaceVersion::UnstableV1, waylandServer()->display())->create(); + } + + conn->setInputConfig(kwinApp()->inputConfig()); + conn->updateLEDs(m_keyboard->xkb()->leds()); + 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)); + 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, uint32_t time) +{ + m_pointer->processAxis(axis, delta, 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; + } + return input.translated(t->pos()).contains(pos); +} + +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 UnmanagedList &unmanaged = Workspace::self()->unmanagedList(); + foreach (Unmanaged *u, unmanaged) { + if (u->geometry().contains(pos) && acceptsInput(u, pos)) { + return u; + } + } + } + const ToplevelList &stacking = Workspace::self()->stackingOrder(); + if (stacking.isEmpty()) { + return NULL; + } + 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->isCurrentTab() || 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 NULL; +} + +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) + , m_input(input) +{ +} + +InputDeviceHandler::~InputDeviceHandler() = default; + +void InputDeviceHandler::updateDecoration(Toplevel *t, const QPointF &pos) +{ + const auto oldDeco = m_decoration; + bool needsReset = waylandServer()->isScreenLocked(); + if (AbstractClient *c = dynamic_cast(t)) { + // check whether it's on a Decoration + if (c->decoratedClient()) { + const QRect clientRect = QRect(c->clientPos(), c->clientSize()).translated(c->pos()); + if (!clientRect.contains(pos.toPoint())) { + m_decoration = c->decoratedClient(); + } else { + needsReset = true; + } + } else { + needsReset = true; + } + } else { + needsReset = true; + } + if (needsReset) { + m_decoration.clear(); + } + + bool leftSend = false; + auto oldWindow = qobject_cast(m_window.data()); + if (oldWindow && (m_decoration && m_decoration->client() != oldWindow)) { + leftSend = true; + oldWindow->leaveEvent(); + } + + if (oldDeco && oldDeco != m_decoration) { + if (oldDeco->client() != t && !leftSend) { + leftSend = true; + oldDeco->client()->leaveEvent(); + } + // send leave + QHoverEvent event(QEvent::HoverLeave, QPointF(), QPointF()); + QCoreApplication::instance()->sendEvent(oldDeco->decoration(), &event); + } + if (m_decoration) { + if (m_decoration->client() != oldWindow) { + m_decoration->client()->enterEvent(pos.toPoint()); + workspace()->updateFocusMousePosition(pos.toPoint()); + } + const QPointF p = pos - t->pos(); + QHoverEvent event(QEvent::HoverMove, p, p); + QCoreApplication::instance()->sendEvent(m_decoration->decoration(), &event); + m_decoration->client()->processDecorationMove(p.toPoint(), pos.toPoint()); + } +} + +void InputDeviceHandler::updateInternalWindow(const QPointF &pos) +{ + const auto oldInternalWindow = m_internalWindow; + bool found = false; + // TODO: screen locked check without going through wayland server + bool needsReset = waylandServer()->isScreenLocked(); + const auto &internalClients = waylandServer()->internalClients(); + const bool change = m_internalWindow.isNull() || !(m_internalWindow->flags().testFlag(Qt::Popup) && m_internalWindow->isVisible()); + if (!internalClients.isEmpty() && change) { + auto it = internalClients.end(); + do { + it--; + if (QWindow *w = (*it)->internalWindow()) { + if (!w->isVisible()) { + continue; + } + if ((*it)->geometry().contains(pos.toPoint())) { + // check input mask + const QRegion mask = w->mask().translated(w->geometry().topLeft()); + if (!mask.isEmpty() && !mask.contains(pos.toPoint())) { + continue; + } + if (w->property("outputOnly").toBool()) { + continue; + } + m_internalWindow = QPointer(w); + found = true; + break; + } + } + } while (it != internalClients.begin()); + if (!found) { + needsReset = true; + } + } + if (needsReset) { + m_internalWindow.clear(); + } + if (oldInternalWindow != m_internalWindow) { + // changed + if (oldInternalWindow) { + QEvent event(QEvent::Leave); + QCoreApplication::sendEvent(oldInternalWindow.data(), &event); + } + if (m_internalWindow) { + QEnterEvent event(pos - m_internalWindow->position(), + pos - m_internalWindow->position(), + pos); + QCoreApplication::sendEvent(m_internalWindow.data(), &event); + } + emit internalWindowChanged(); + } +} + +} // namespace diff --git a/input.h b/input.h new file mode 100644 index 0000000..0a1efab --- /dev/null +++ b/input.h @@ -0,0 +1,423 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_INPUT_H +#define KWIN_INPUT_H +#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 TouchInputRedirection; +class WindowSelectorFilter; +class SwitchEvent; + +namespace Decoration +{ +class DecoratedClientImpl; +} + +namespace LibInput +{ + class Connection; +} + +/** + * @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 KeyboardKeyState { + KeyboardKeyReleased, + KeyboardKeyPressed, + KeyboardKeyAutoRepeat + }; + virtual ~InputRedirection(); + 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, 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); + 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; + } + 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 oldMods 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; + TouchInputRedirection *m_touch; + + 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(quint32 id, const QPointF &pos, quint32 time); + virtual bool touchMotion(quint32 id, const QPointF &pos, quint32 time); + virtual bool touchUp(quint32 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); + +protected: + void passToWaylandServer(QKeyEvent *event); +}; + +class InputDeviceHandler : public QObject +{ + Q_OBJECT +public: + virtual ~InputDeviceHandler(); + + QPointer window() const { + return m_window; + } + QPointer decoration() const { + return m_decoration; + } + QPointer internalWindow() const { + return m_internalWindow; + } + +Q_SIGNALS: + void decorationChanged(); + void internalWindowChanged(); + +protected: + explicit InputDeviceHandler(InputRedirection *parent); + void updateDecoration(Toplevel *t, const QPointF &pos); + void updateInternalWindow(const QPointF &pos); + InputRedirection *m_input; + /** + * @brief The Toplevel which currently receives events + */ + QPointer m_window; + /** + * @brief The Decoration which currently receives events. + **/ + QPointer m_decoration; + QPointer m_internalWindow; +}; + +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) + +#endif // KWIN_INPUT_H diff --git a/input_event.cpp b/input_event.cpp new file mode 100644 index 0000000..b8a2d7c --- /dev/null +++ b/input_event.cpp @@ -0,0 +1,63 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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, Qt::Orientation orientation, Qt::MouseButtons buttons, + Qt::KeyboardModifiers modifiers, 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) +{ + 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); +} + +} diff --git a/input_event.h b/input_event.h new file mode 100644 index 0000000..7854a10 --- /dev/null +++ b/input_event.h @@ -0,0 +1,156 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_INPUT_EVENT_H +#define KWIN_INPUT_EVENT_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; +}; + +class WheelEvent : public QWheelEvent +{ +public: + explicit WheelEvent(const QPointF &pos, qreal delta, Qt::Orientation orientation, Qt::MouseButtons buttons, + Qt::KeyboardModifiers modifiers, 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 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; +}; + +} + +#endif diff --git a/input_event_spy.cpp b/input_event_spy.cpp new file mode 100644 index 0000000..bb139c0 --- /dev/null +++ b/input_event_spy.cpp @@ -0,0 +1,124 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(quint32 id, const QPointF &point, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(point) + Q_UNUSED(time) +} + +void InputEventSpy::touchMotion(quint32 id, const QPointF &point, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(point) + Q_UNUSED(time) +} + +void InputEventSpy::touchUp(quint32 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) +} + +} diff --git a/input_event_spy.h b/input_event_spy.h new file mode 100644 index 0000000..32ba36b --- /dev/null +++ b/input_event_spy.h @@ -0,0 +1,92 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_INPUT_EVENT_SPY_H +#define KWIN_INPUT_EVENT_SPY_H +#include + +#include + +class QPointF; +class QSizeF; + +namespace KWin +{ +class KeyEvent; +class MouseEvent; +class WheelEvent; +class SwitchEvent; + + +/** + * 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(quint32 id, const QPointF &pos, quint32 time); + virtual void touchMotion(quint32 id, const QPointF &pos, quint32 time); + virtual void touchUp(quint32 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); + +}; + + +} // namespace KWin + +#endif diff --git a/kcmkwin/CMakeLists.txt b/kcmkwin/CMakeLists.txt new file mode 100644 index 0000000..19e1ee6 --- /dev/null +++ b/kcmkwin/CMakeLists.txt @@ -0,0 +1,13 @@ +remove_definitions(-DQT_NO_CAST_FROM_ASCII -DQT_STRICT_ITERATORS -DQT_NO_CAST_FROM_BYTEARRAY -DQT_NO_KEYWORDS) + +add_subdirectory( kwincompositing ) +add_subdirectory( kwinoptions ) +add_subdirectory( kwindecoration ) +add_subdirectory( kwinrules ) +add_subdirectory( kwinscreenedges ) +add_subdirectory( kwinscripts ) +add_subdirectory( kwindesktop ) + +if( KWIN_BUILD_TABBOX ) +add_subdirectory( kwintabbox ) +endif() diff --git a/kcmkwin/kwincompositing/CMakeLists.txt b/kcmkwin/kwincompositing/CMakeLists.txt new file mode 100644 index 0000000..56a5a7d --- /dev/null +++ b/kcmkwin/kwincompositing/CMakeLists.txt @@ -0,0 +1,87 @@ +######################################################################### +# 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) + +include_directories(${KWIN_SOURCE_DIR}/effects) + +################# configure checks and create the configured files ################# + +# now create config headers +configure_file(config-prefix.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-prefix.h ) +configure_file(config-compiler.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-compiler.h ) + +set(kwincomposing_SRC + model.cpp + main.cpp + compositing.cpp + effectconfig.cpp) + +qt5_add_dbus_interface( kwincomposing_SRC + ${KWIN_SOURCE_DIR}/org.kde.kwin.Compositing.xml kwin_compositing_interface) +qt5_add_dbus_interface( kwincomposing_SRC + ${KWIN_SOURCE_DIR}/org.kde.kwin.Effects.xml kwin_effects_interface) + +ki18n_wrap_ui(kwincomposing_SRC compositing.ui) + +add_library(kwincompositing MODULE ${kwincomposing_SRC}) + +target_link_libraries(kwincompositing + Qt5::Quick + Qt5::QuickWidgets + Qt5::DBus + Qt5::Widgets + KF5::CoreAddons + KF5::ConfigCore + KF5::Declarative + KF5::I18n + KF5::Service + KF5::KCMUtils + KF5::NewStuff + kwin4_effect_builtins +) + +if (BUILD_TESTING) + include(ECMMarkAsTest) + + set(modelTest_SRC + model.cpp + effectconfig.cpp + compositing.cpp + test/effectmodeltest.cpp + test/modeltest.cpp) + + qt5_add_dbus_interface(modelTest_SRC + ${KWIN_SOURCE_DIR}/org.kde.kwin.Compositing.xml kwin_compositing_interface) + qt5_add_dbus_interface(modelTest_SRC + ${KWIN_SOURCE_DIR}/org.kde.kwin.Effects.xml kwin_effects_interface) + + add_executable(effectModelTest ${modelTest_SRC}) + ecm_mark_as_test(effectModelTest) + + target_link_libraries(effectModelTest + Qt5::Quick + Qt5::QuickWidgets + Qt5::DBus + Qt5::Test + Qt5::Widgets + KF5::CoreAddons + KF5::ConfigCore + KF5::Declarative + KF5::I18n + KF5::Service + KF5::KCMUtils + KF5::NewStuff + kwineffects + kwin4_effect_builtins + ) +endif() + +INSTALL(DIRECTORY qml DESTINATION ${DATA_INSTALL_DIR}/kwincompositing) +INSTALL(TARGETS kwincompositing DESTINATION ${PLUGIN_INSTALL_DIR}) +install(FILES kwincompositing.desktop kcmkwineffects.desktop DESTINATION ${SERVICES_INSTALL_DIR}) +install(FILES kwineffect.knsrc DESTINATION ${CONFIG_INSTALL_DIR}) +################# list the subdirectories ################# diff --git a/kcmkwin/kwincompositing/Messages.sh b/kcmkwin/kwincompositing/Messages.sh new file mode 100644 index 0000000..f8ca2ec --- /dev/null +++ b/kcmkwin/kwincompositing/Messages.sh @@ -0,0 +1,3 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.ui` >> rc.cpp || exit 11 +$XGETTEXT *.h *.cpp qml/*.qml -o $podir/kcmkwincompositing.pot diff --git a/kcmkwin/kwincompositing/compositing.cpp b/kcmkwin/kwincompositing/compositing.cpp new file mode 100644 index 0000000..102dfd1 --- /dev/null +++ b/kcmkwin/kwincompositing/compositing.cpp @@ -0,0 +1,532 @@ +/************************************************************************** +* KWin - the KDE window manager * +* This file is part of the KDE project. * +* * +* Copyright (C) 2013 Antonis Tsiapaliokas * +* Copyright (C) 2013 Martin Gräßlin * +* * +* 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, see . * +**************************************************************************/ + +#include "compositing.h" +#include + +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace KWin { +namespace Compositing { + +Compositing::Compositing(QObject *parent) + : QObject(parent) + , m_animationSpeed(0) + , m_windowThumbnail(0) + , m_glScaleFilter(0) + , m_xrScaleFilter(false) + , m_glSwapStrategy(0) + , m_compositingType(0) + , m_compositingEnabled(true) + , m_changed(false) + , m_openGLPlatformInterfaceModel(new OpenGLPlatformInterfaceModel(this)) + , m_openGLPlatformInterface(0) + , m_windowsBlockCompositing(true) + , m_compositingInterface(new OrgKdeKwinCompositingInterface(QStringLiteral("org.kde.KWin"), QStringLiteral("/Compositor"), QDBusConnection::sessionBus(), this)) +{ + reset(); + connect(this, &Compositing::animationSpeedChanged, this, &Compositing::changed); + connect(this, &Compositing::windowThumbnailChanged, this, &Compositing::changed); + connect(this, &Compositing::glScaleFilterChanged, this, &Compositing::changed); + connect(this, &Compositing::xrScaleFilterChanged, this, &Compositing::changed); + connect(this, &Compositing::glSwapStrategyChanged, this, &Compositing::changed); + connect(this, &Compositing::compositingTypeChanged, this, &Compositing::changed); + connect(this, &Compositing::compositingEnabledChanged, this, &Compositing::changed); + connect(this, &Compositing::openGLPlatformInterfaceChanged, this, &Compositing::changed); + connect(this, &Compositing::windowsBlockCompositingChanged, this, &Compositing::changed); + + connect(this, &Compositing::changed, [this]{ + m_changed = true; + }); +} + +void Compositing::reset() +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig(QStringLiteral("kwinrc")), QStringLiteral("Compositing")); + setAnimationSpeed(kwinConfig.readEntry("AnimationSpeed", 3)); + setWindowThumbnail(kwinConfig.readEntry("HiddenPreviews", 5) - 4); + setGlScaleFilter(kwinConfig.readEntry("GLTextureFilter", 2)); + setXrScaleFilter(kwinConfig.readEntry("XRenderSmoothScale", false)); + setCompositingEnabled(kwinConfig.readEntry("Enabled", true)); + + auto swapStrategy = [&kwinConfig]() { + const QString glSwapStrategyValue = kwinConfig.readEntry("GLPreferBufferSwap", "a"); + + if (glSwapStrategyValue == "n") { + return 0; + } else if (glSwapStrategyValue == "a") { + return 1; + } else if (glSwapStrategyValue == "e") { + return 2; + } else if (glSwapStrategyValue == "p") { + return 3; + } else if (glSwapStrategyValue == "c") { + return 4; + } + return 0; + }; + setGlSwapStrategy(swapStrategy()); + + auto type = [&kwinConfig]{ + const QString backend = kwinConfig.readEntry("Backend", "OpenGL"); + const bool glCore = kwinConfig.readEntry("GLCore", false); + + if (backend == QStringLiteral("OpenGL")) { + if (glCore) { + return CompositingType::OPENGL31_INDEX; + } else { + return CompositingType::OPENGL20_INDEX; + } + } else { + return CompositingType::XRENDER_INDEX; + } + }; + setCompositingType(type()); + + const QModelIndex index = m_openGLPlatformInterfaceModel->indexForKey(kwinConfig.readEntry("GLPlatformInterface", "glx")); + setOpenGLPlatformInterface(index.isValid() ? index.row() : 0); + + setWindowsBlockCompositing(kwinConfig.readEntry("WindowsBlockCompositing", true)); + + m_changed = false; +} + +void Compositing::defaults() +{ + setAnimationSpeed(3); + setWindowThumbnail(1); + setGlScaleFilter(2); + setXrScaleFilter(false); + setGlSwapStrategy(1); + setCompositingType(CompositingType::OPENGL20_INDEX); + const QModelIndex index = m_openGLPlatformInterfaceModel->indexForKey(QStringLiteral("glx")); + setOpenGLPlatformInterface(index.isValid() ? index.row() : 0); + setWindowsBlockCompositing(true); + m_changed = true; +} + +bool Compositing::OpenGLIsUnsafe() const +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), "Compositing"); + return kwinConfig.readEntry("OpenGLIsUnsafe", true); +} + +bool Compositing::OpenGLIsBroken() +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), "Compositing"); + + QString oldBackend = kwinConfig.readEntry("Backend", "OpenGL"); + kwinConfig.writeEntry("Backend", "OpenGL"); + kwinConfig.sync(); + + if (m_compositingInterface->openGLIsBroken()) { + kwinConfig.writeEntry("Backend", oldBackend); + kwinConfig.sync(); + return true; + } + + kwinConfig.writeEntry("OpenGLIsUnsafe", false); + kwinConfig.sync(); + return false; +} + +void Compositing::reenableOpenGLDetection() +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), "Compositing"); + kwinConfig.writeEntry("OpenGLIsUnsafe", false); + kwinConfig.sync(); +} + +int Compositing::animationSpeed() const +{ + return m_animationSpeed; +} + +int Compositing::windowThumbnail() const +{ + return m_windowThumbnail; +} + +int Compositing::glScaleFilter() const +{ + return m_glScaleFilter; +} + +bool Compositing::xrScaleFilter() const +{ + return m_xrScaleFilter; +} + +int Compositing::glSwapStrategy() const +{ + return m_glSwapStrategy; +} + +int Compositing::compositingType() const +{ + return m_compositingType; +} + +bool Compositing::compositingEnabled() const +{ + return m_compositingEnabled; +} + +void Compositing::setAnimationSpeed(int speed) +{ + if (speed == m_animationSpeed) { + return; + } + m_animationSpeed = speed; + emit animationSpeedChanged(speed); +} + +void Compositing::setGlScaleFilter(int index) +{ + if (index == m_glScaleFilter) { + return; + } + m_glScaleFilter = index; + emit glScaleFilterChanged(index); +} + +void Compositing::setGlSwapStrategy(int strategy) +{ + if (strategy == m_glSwapStrategy) { + return; + } + m_glSwapStrategy = strategy; + emit glSwapStrategyChanged(strategy); +} + +void Compositing::setWindowThumbnail(int index) +{ + if (index == m_windowThumbnail) { + return; + } + m_windowThumbnail = index; + emit windowThumbnailChanged(index); +} + +void Compositing::setXrScaleFilter(bool filter) +{ + if (filter == m_xrScaleFilter) { + return; + } + m_xrScaleFilter = filter; + emit xrScaleFilterChanged(filter); +} + +void Compositing::setCompositingType(int index) +{ + if (index == m_compositingType) { + return; + } + m_compositingType = index; + emit compositingTypeChanged(index); +} + +void Compositing::setCompositingEnabled(bool enabled) +{ + if (compositingRequired()) { + return; + } + if (enabled == m_compositingEnabled) { + return; + } + + m_compositingEnabled = enabled; + emit compositingEnabledChanged(enabled); +} + +void Compositing::save() +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig(QStringLiteral("kwinrc")), "Compositing"); + kwinConfig.writeEntry("AnimationSpeed", animationSpeed()); + kwinConfig.writeEntry("HiddenPreviews", windowThumbnail() + 4); + kwinConfig.writeEntry("GLTextureFilter", glScaleFilter()); + kwinConfig.writeEntry("XRenderSmoothScale", xrScaleFilter()); + if (!compositingRequired()) { + kwinConfig.writeEntry("Enabled", compositingEnabled()); + } + auto swapStrategy = [this] { + switch (glSwapStrategy()) { + case 0: + return QStringLiteral("n"); + case 2: + return QStringLiteral("e"); + case 3: + return QStringLiteral("p"); + case 4: + return QStringLiteral("c"); + case 1: + default: + return QStringLiteral("a"); + } + }; + kwinConfig.writeEntry("GLPreferBufferSwap", swapStrategy()); + QString backend; + bool glCore = false; + switch (compositingType()) { + case CompositingType::OPENGL31_INDEX: + backend = "OpenGL"; + glCore = true; + break; + case CompositingType::OPENGL20_INDEX: + backend = "OpenGL"; + glCore = false; + break; + case CompositingType::XRENDER_INDEX: + backend = "XRender"; + glCore = false; + break; + } + kwinConfig.writeEntry("Backend", backend); + kwinConfig.writeEntry("GLCore", glCore); + if (!compositingRequired()) { + kwinConfig.writeEntry("WindowsBlockCompositing", windowsBlockCompositing()); + } + kwinConfig.sync(); + + if (m_changed) { + // Send signal to all kwin instances + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/Compositor"), + QStringLiteral("org.kde.kwin.Compositing"), + QStringLiteral("reinit")); + QDBusConnection::sessionBus().send(message); + m_changed = false; + } +} + +OpenGLPlatformInterfaceModel *Compositing::openGLPlatformInterfaceModel() const +{ + return m_openGLPlatformInterfaceModel; +} + +int Compositing::openGLPlatformInterface() const +{ + return m_openGLPlatformInterface; +} + +void Compositing::setOpenGLPlatformInterface(int interface) +{ + if (m_openGLPlatformInterface == interface) { + return; + } + m_openGLPlatformInterface = interface; + emit openGLPlatformInterfaceChanged(interface); +} + +bool Compositing::windowsBlockCompositing() const +{ + return m_windowsBlockCompositing; +} + +void Compositing::setWindowsBlockCompositing(bool set) +{ + if (compositingRequired()) { + return; + } + if (m_windowsBlockCompositing == set) { + return; + } + m_windowsBlockCompositing = set; + emit windowsBlockCompositingChanged(set); +} + +bool Compositing::compositingRequired() const +{ + return m_compositingInterface->platformRequiresCompositing(); +} + +CompositingType::CompositingType(QObject *parent) + : QAbstractItemModel(parent) { + + generateCompositing(); +} + +void CompositingType::generateCompositing() +{ + QHash compositingTypes; + + compositingTypes[i18n("OpenGL 3.1")] = CompositingType::OPENGL31_INDEX; + compositingTypes[i18n("OpenGL 2.0")] = CompositingType::OPENGL20_INDEX; + compositingTypes[i18n("XRender")] = CompositingType::XRENDER_INDEX; + + CompositingData data; + beginResetModel(); + auto it = compositingTypes.begin(); + while (it != compositingTypes.end()) { + data.name = it.key(); + data.type = it.value(); + m_compositingList << data; + it++; + } + + qSort(m_compositingList.begin(), m_compositingList.end(), [](const CompositingData &a, const CompositingData &b) { + return a.type < b.type; + }); + endResetModel(); +} + +QHash< int, QByteArray > CompositingType::roleNames() const +{ + QHash roleNames; + roleNames[NameRole] = "NameRole"; + roleNames[TypeRole] = QByteArrayLiteral("type"); + return roleNames; +} + +QModelIndex CompositingType::index(int row, int column, const QModelIndex &parent) const +{ + +if (parent.isValid() || column > 0 || column < 0 || row < 0 || row >= m_compositingList.count()) { + return QModelIndex(); + } + + return createIndex(row, column); +} + +QModelIndex CompositingType::parent(const QModelIndex &child) const +{ + Q_UNUSED(child) + + return QModelIndex(); +} + +int CompositingType::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 1; +} + +int CompositingType::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_compositingList.count(); +} + +QVariant CompositingType::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + switch (role) { + case Qt::DisplayRole: + case NameRole: + return m_compositingList.at(index.row()).name; + case TypeRole: + return m_compositingList.at(index.row()).type; + default: + return QVariant(); + } +} + +int CompositingType::compositingTypeForIndex(int row) const +{ + return index(row, 0).data(TypeRole).toInt(); +} + +int CompositingType::indexForCompositingType(int type) const +{ + for (int i = 0; i < m_compositingList.count(); ++i) { + if (m_compositingList.at(i).type == type) { + return i; + } + } + return -1; +} + +OpenGLPlatformInterfaceModel::OpenGLPlatformInterfaceModel(QObject *parent) + : QAbstractListModel(parent) +{ + beginResetModel(); + OrgKdeKwinCompositingInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Compositor"), + QDBusConnection::sessionBus()); + m_keys << interface.supportedOpenGLPlatformInterfaces(); + for (const QString &key : m_keys) { + if (key == QStringLiteral("egl")) { + m_names << i18nc("OpenGL Platform Interface", "EGL"); + } else if (key == QStringLiteral("glx")) { + m_names << i18nc("OpenGL Platform Interface", "GLX"); + } else { + m_names << key; + } + } + endResetModel(); +} + +OpenGLPlatformInterfaceModel::~OpenGLPlatformInterfaceModel() = default; + +int OpenGLPlatformInterfaceModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_keys.count(); +} + +QHash< int, QByteArray > OpenGLPlatformInterfaceModel::roleNames() const +{ + return QHash({ + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {Qt::UserRole, QByteArrayLiteral("openglPlatformInterface")} + }); +} + +QVariant OpenGLPlatformInterfaceModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid() || index.row() < 0 || index.row() >= m_keys.size() || index.column() != 0) { + return QVariant(); + } + switch (role) { + case Qt::DisplayRole: + return m_names.at(index.row()); + case Qt::UserRole: + return m_keys.at(index.row()); + default: + return QVariant(); + } +} + +QModelIndex OpenGLPlatformInterfaceModel::indexForKey(const QString &key) const +{ + const int keyIndex = m_keys.indexOf(key); + if (keyIndex < 0) { + return QModelIndex(); + } + return createIndex(keyIndex, 0); +} + +}//end namespace Compositing +}//end namespace KWin diff --git a/kcmkwin/kwincompositing/compositing.h b/kcmkwin/kwincompositing/compositing.h new file mode 100644 index 0000000..80c2815 --- /dev/null +++ b/kcmkwin/kwincompositing/compositing.h @@ -0,0 +1,181 @@ +/************************************************************************** + * KWin - the KDE window manager * + * This file is part of the KDE project. * + * * + * Copyright (C) 2013 Antonis Tsiapaliokas * + * * + * 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, see . * + **************************************************************************/ + + +#ifndef COMPOSITING_H +#define COMPOSITING_H + +#include +#include + +class OrgKdeKwinCompositingInterface; + +namespace KWin { +namespace Compositing { + +class OpenGLPlatformInterfaceModel; + +class Compositing : public QObject +{ + + Q_OBJECT + Q_PROPERTY(int animationSpeed READ animationSpeed WRITE setAnimationSpeed NOTIFY animationSpeedChanged) + Q_PROPERTY(int windowThumbnail READ windowThumbnail WRITE setWindowThumbnail NOTIFY windowThumbnailChanged) + Q_PROPERTY(int glScaleFilter READ glScaleFilter WRITE setGlScaleFilter NOTIFY glScaleFilterChanged) + Q_PROPERTY(bool xrScaleFilter READ xrScaleFilter WRITE setXrScaleFilter NOTIFY xrScaleFilterChanged) + Q_PROPERTY(int glSwapStrategy READ glSwapStrategy WRITE setGlSwapStrategy NOTIFY glSwapStrategyChanged) + Q_PROPERTY(int compositingType READ compositingType WRITE setCompositingType NOTIFY compositingTypeChanged) + Q_PROPERTY(bool compositingEnabled READ compositingEnabled WRITE setCompositingEnabled NOTIFY compositingEnabledChanged) + Q_PROPERTY(KWin::Compositing::OpenGLPlatformInterfaceModel *openGLPlatformInterfaceModel READ openGLPlatformInterfaceModel CONSTANT) + Q_PROPERTY(int openGLPlatformInterface READ openGLPlatformInterface WRITE setOpenGLPlatformInterface NOTIFY openGLPlatformInterfaceChanged) + Q_PROPERTY(bool windowsBlockCompositing READ windowsBlockCompositing WRITE setWindowsBlockCompositing NOTIFY windowsBlockCompositingChanged) + Q_PROPERTY(bool compositingRequired READ compositingRequired CONSTANT) +public: + explicit Compositing(QObject *parent = 0); + + Q_INVOKABLE bool OpenGLIsUnsafe() const; + Q_INVOKABLE bool OpenGLIsBroken(); + Q_INVOKABLE void reenableOpenGLDetection(); + int animationSpeed() const; + int windowThumbnail() const; + int glScaleFilter() const; + bool xrScaleFilter() const; + int glSwapStrategy() const; + int compositingType() const; + bool compositingEnabled() const; + int openGLPlatformInterface() const; + bool windowsBlockCompositing() const; + bool compositingRequired() const; + + OpenGLPlatformInterfaceModel *openGLPlatformInterfaceModel() const; + + void setAnimationSpeed(int speed); + void setWindowThumbnail(int index); + void setGlScaleFilter(int index); + void setXrScaleFilter(bool filter); + void setGlSwapStrategy(int strategy); + void setCompositingType(int index); + void setCompositingEnabled(bool enalbed); + void setOpenGLPlatformInterface(int interface); + void setWindowsBlockCompositing(bool set); + + void save(); + +public Q_SLOTS: + void reset(); + void defaults(); + +Q_SIGNALS: + void changed(); + void animationSpeedChanged(int); + void windowThumbnailChanged(int); + void glScaleFilterChanged(int); + void xrScaleFilterChanged(int); + void glSwapStrategyChanged(int); + void compositingTypeChanged(int); + void compositingEnabledChanged(bool); + void openGLPlatformInterfaceChanged(int); + void windowsBlockCompositingChanged(bool); + +private: + int m_animationSpeed; + int m_windowThumbnail; + int m_glScaleFilter; + bool m_xrScaleFilter; + int m_glSwapStrategy; + int m_compositingType; + bool m_compositingEnabled; + bool m_changed; + OpenGLPlatformInterfaceModel *m_openGLPlatformInterfaceModel; + int m_openGLPlatformInterface; + bool m_windowsBlockCompositing; + bool m_windowsBlockingCompositing; + OrgKdeKwinCompositingInterface *m_compositingInterface; +}; + + +struct CompositingData; + +class CompositingType : public QAbstractItemModel +{ + + Q_OBJECT + Q_ENUMS(CompositingTypeIndex) + +public: + + enum CompositingTypeIndex { + OPENGL31_INDEX = 0, + OPENGL20_INDEX, + XRENDER_INDEX + }; + + enum CompositingTypeRoles { + NameRole = Qt::UserRole +1, + TypeRole = Qt::UserRole +2 + }; + + explicit CompositingType(QObject *parent = 0); + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + virtual QHash< int, QByteArray > roleNames() const override; + + Q_INVOKABLE int compositingTypeForIndex(int row) const; + Q_INVOKABLE int indexForCompositingType(int type) const; + +private: + void generateCompositing(); + QList m_compositingList; + +}; + +struct CompositingData { + QString name; + CompositingType::CompositingTypeIndex type; +}; + +class OpenGLPlatformInterfaceModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit OpenGLPlatformInterfaceModel(QObject *parent = nullptr); + virtual ~OpenGLPlatformInterfaceModel(); + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QModelIndex indexForKey(const QString &key) const; + + QHash< int, QByteArray > roleNames() const override; + +private: + QStringList m_keys; + QStringList m_names; +}; + +}//end namespace Compositing +}//end namespace KWin + +Q_DECLARE_METATYPE(KWin::Compositing::OpenGLPlatformInterfaceModel*) +#endif diff --git a/kcmkwin/kwincompositing/compositing.ui b/kcmkwin/kwincompositing/compositing.ui new file mode 100644 index 0000000..f8a8bca --- /dev/null +++ b/kcmkwin/kwincompositing/compositing.ui @@ -0,0 +1,296 @@ + + + CompositingForm + + + + 0 + 0 + 462 + 349 + + + + + QFormLayout::AllNonFixedFieldsGrow + + + + + + + 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: + + + + + + + + + 6 + + + Qt::Horizontal + + + QSlider::TicksBelow + + + 1 + + + + + + + + + Instant + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Very slow + + + + + + + + + + + Scale method: + + + + + + + + Crisp + + + + + Smooth + + + + + Accurate + + + + + + + + Scale method: + + + + + + + + Crisp + + + + + Smooth (slower) + + + + + + + + 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/config-compiler.h.cmake b/kcmkwin/kwincompositing/config-compiler.h.cmake new file mode 100644 index 0000000..c389841 --- /dev/null +++ b/kcmkwin/kwincompositing/config-compiler.h.cmake @@ -0,0 +1,7 @@ + +#define KDE_COMPILER_VERSION "${KDE_COMPILER_VERSION}" + +#define KDE_COMPILING_OS "${CMAKE_SYSTEM}" + +#define KDE_DISTRIBUTION_TEXT "${KDE_DISTRIBUTION_TEXT}" + diff --git a/kcmkwin/kwincompositing/config-prefix.h.cmake b/kcmkwin/kwincompositing/config-prefix.h.cmake new file mode 100644 index 0000000..9d15b5f --- /dev/null +++ b/kcmkwin/kwincompositing/config-prefix.h.cmake @@ -0,0 +1,34 @@ +/* This file contains all the paths that change when changing the installation prefix */ + +#define CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}" + +/* The compiled in system configuration prefix */ +#define KDESYSCONFDIR "${SYSCONF_INSTALL_DIR}" + +#define __KDE_BINDIR "${BIN_INSTALL_DIR}" + +/* Compile in the exec prefix to help kstddirs in finding dynamic libs + (This was for exec_prefix != prefix - still needed?) */ +#define __KDE_EXECPREFIX "NONE" + +#define LIBEXEC_INSTALL_DIR "${LIBEXEC_INSTALL_DIR}" +#define DATA_INSTALL_DIR "${DATA_INSTALL_DIR}" +#define LIB_INSTALL_DIR "${LIB_INSTALL_DIR}" +#define INCLUDE_INSTALL_DIR "${INCLUDE_INSTALL_DIR}" +#define BIN_INSTALL_DIR "${BIN_INSTALL_DIR}" +#define CONFIG_INSTALL_DIR "${CONFIG_INSTALL_DIR}" +#define HTML_INSTALL_DIR "${HTML_INSTALL_DIR}" +#define ICON_INSTALL_DIR "${ICON_INSTALL_DIR}" +#define KCFG_INSTALL_DIR "${KCFG_INSTALL_DIR}" +#define PLUGIN_INSTALL_DIR "${PLUGIN_INSTALL_DIR}" +#define SERVICES_INSTALL_DIR "${SERVICES_INSTALL_DIR}" +#define SERVICETYPES_INSTALL_DIR "${SERVICETYPES_INSTALL_DIR}" +#define SOUND_INSTALL_DIR "${SOUND_INSTALL_DIR}" +#define TEMPLATES_INSTALL_DIR "${TEMPLATES_INSTALL_DIR}" +#define WALLPAPER_INSTALL_DIR "${WALLPAPER_INSTALL_DIR}" +#define XDG_APPS_INSTALL_DIR "${XDG_APPS_INSTALL_DIR}" +#define XDG_DIRECTORY_INSTALL_DIR "${XDG_DIRECTORY_INSTALL_DIR}" +#define EXEC_INSTALL_PREFIX "${EXEC_INSTALL_PREFIX}" +#define SYSCONF_INSTALL_DIR "${SYSCONF_INSTALL_DIR}" +#define LOCALE_INSTALL_DIR "${LOCALE_INSTALL_DIR}" +#define SYSCONF_INSTALL_DIR "${SYSCONF_INSTALL_DIR}" diff --git a/kcmkwin/kwincompositing/effectconfig.cpp b/kcmkwin/kwincompositing/effectconfig.cpp new file mode 100644 index 0000000..ee4eb03 --- /dev/null +++ b/kcmkwin/kwincompositing/effectconfig.cpp @@ -0,0 +1,115 @@ +/************************************************************************** +* KWin - the KDE window manager * +* This file is part of the KDE project. * +* * +* Copyright (C) 2013 Antonis Tsiapaliokas * +* * +* 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, see . * +**************************************************************************/ + +#include "effectconfig.h" + +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +static const QString s_pluginDir = QStringLiteral("kwin/effects/configs/"); + +namespace KWin { +namespace Compositing { + +EffectConfig::EffectConfig(QObject *parent) + : QObject(parent) +{ +} + +void EffectConfig::openConfig(const QString &serviceName, bool scripted, const QString &title) +{ + //setup the UI + QDialog dialog; + dialog.setWindowTitle(title); + + // create the KCModule through the plugintrader + KCModule *kcm = nullptr; + if (scripted) { + // try generic module for scripted + const auto offers = KPluginTrader::self()->query(s_pluginDir, QString(), + QStringLiteral("[X-KDE-Library] == 'kcm_kwin4_genericscripted'")); + if (!offers.isEmpty()) { + const KPluginInfo &generic = offers.first(); + KPluginLoader loader(generic.libraryPath()); + KPluginFactory *factory = loader.factory(); + if (factory) { + kcm = factory->create(serviceName, &dialog); + } + } + } else { + kcm = KPluginTrader::createInstanceFromQuery(s_pluginDir, QString(), + QStringLiteral("'%1' in [X-KDE-ParentComponents]").arg(serviceName), + &dialog); + } + if (!kcm) { + return; + } + + connect(&dialog, &QDialog::accepted, kcm, &KCModule::save); + + QDialogButtonBox *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | + QDialogButtonBox::Cancel | + QDialogButtonBox::Apply | + QDialogButtonBox::RestoreDefaults | + QDialogButtonBox::Reset, + &dialog); + + QPushButton *apply = buttons->button(QDialogButtonBox::Apply); + QPushButton *reset = buttons->button(QDialogButtonBox::Reset); + apply->setEnabled(false); + 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(apply, &QPushButton::clicked, kcm, &KCModule::save); + connect(reset, &QPushButton::clicked, kcm, &KCModule::load); + auto changedSignal = static_cast(&KCModule::changed); + connect(kcm, changedSignal, apply, &QPushButton::setEnabled); + 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); + dialog.exec(); +} + +void EffectConfig::openGHNS() +{ + QPointer downloadDialog = new KNS3::DownloadDialog(QStringLiteral("kwineffect.knsrc")); + if (downloadDialog->exec() == QDialog::Accepted) { + emit effectListChanged(); + } + + delete downloadDialog; +} + +}//end namespace Compositing +}//end namespace KWin diff --git a/kcmkwin/kwincompositing/effectconfig.h b/kcmkwin/kwincompositing/effectconfig.h new file mode 100644 index 0000000..73e20d1 --- /dev/null +++ b/kcmkwin/kwincompositing/effectconfig.h @@ -0,0 +1,47 @@ +/************************************************************************** + * KWin - the KDE window manager * + * This file is part of the KDE project. * + * * + * Copyright (C) 2013 Antonis Tsiapaliokas * + * * + * 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, see . * + **************************************************************************/ + + +#ifndef EFFECTCONFIG_H +#define EFFECTCONFIG_H +#include +class QString; + +namespace KWin { +namespace Compositing { + +class EffectConfig : public QObject +{ + + Q_OBJECT + +public: + explicit EffectConfig(QObject *parent = 0); + QString serviceName(const QString &serviceName); + + Q_INVOKABLE void openConfig(const QString &effectName, bool scripted, const QString &title); + Q_INVOKABLE void openGHNS(); + +Q_SIGNALS: + void effectListChanged(); +}; +}//end namespace Compositing +}//end namespace KWin +#endif diff --git a/kcmkwin/kwincompositing/kcmkwineffects.desktop b/kcmkwin/kwincompositing/kcmkwineffects.desktop new file mode 100644 index 0000000..0d7e75e --- /dev/null +++ b/kcmkwin/kwincompositing/kcmkwineffects.desktop @@ -0,0 +1,130 @@ +[Desktop Entry] +Exec=kcmshell5 kwineffects +Icon=preferences-desktop +Type=Service +X-KDE-ServiceTypes=KCModule +X-DocPath=kcontrol/kwineffects/index.html + +X-KDE-Library=kwincompositing +X-KDE-PluginKeyword=effects +X-KDE-ParentApp=kcontrol + +X-KDE-System-Settings-Parent-Category=desktopbehavior +X-KDE-Weight=50 + +Name=Desktop Effects +Name[bs]=Efekti površi +Name[ca]=Efectes d'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]=Desktop Effects +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[tr]=Masaüstü Efektleri +Name[uk]=Ефекти стільниці +Name[x-test]=xxDesktop Effectsxx +Name[zh_CN]=桌面特效 +Name[zh_TW]=桌面效果 +Comment=Desktop Effects +Comment[bs]=Efekti površi +Comment[ca]=Efectes d'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[tr]=Masaüstü Efektleri +Comment[uk]=Ефекти стільниці +Comment[x-test]=xxDesktop 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[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 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 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 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 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 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 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[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[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[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[id]=kwin,jendela,pengelola,efek,efek 3D,efek 2D,efek grafik,efek desktop,animasi,beragam animasi,efek pengelola jendela,efek beralih jendela,efek beralih desktop,animasi,animasi desktop,driver,setelan driver,rendering,render,efek kebalikan,efek seperti gelas,efek kaca pembesar,efek pembantu patah,efek lacak mouse,efek pembesaran,efek buram,efek lesap,efek desktop lesap,efek hancur,efek petak,efek jendela 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 thumbnail disamping,translusensi,efek translusensi,transparan,efek geometri jendela,efek jendela goyang,efek tanggapan startup,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 jendela hadir,efek ubah ukuran jendela,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[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,utloggingsefffekt,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 da área de trabalho,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 +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/kwincompositing/kwincompositing.desktop b/kcmkwin/kwincompositing/kwincompositing.desktop new file mode 100644 index 0000000..042adac --- /dev/null +++ b/kcmkwin/kwincompositing/kwincompositing.desktop @@ -0,0 +1,137 @@ +[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[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]=Összeállító +Name[ia]=Compositor +Name[id]=Compositor +Name[it]=Compositore +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[bs]=Postavke Compositor-a za Desktop Efekte +Comment[ca]=Arranjament del compositor pels efectes d'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]=Setelan Kompositor untuk Efek Jendela +Comment[it]=Impostazioni del compositore per gli effetti del desktop +Comment[ko]=데스크톱 효과에 사용되는 컴포지터 설정 +Comment[lt]=Kompozitoriaus nustatymai skirti darbalaukio efektams +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[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,animation speed +X-KDE-Keywords[ca]=kwin,finestra,gestor,composició,efecte,efectes 3D,efectes 2D,OpenGL,XRender,arranjament de vídeo,efectes gràfics,efectes d'escriptori,velocitat de les animacions +X-KDE-Keywords[ca@valencia]=kwin,finestra,gestor,composició,efecte,efectes 3D,efectes 2D,OpenGL,XRender,arranjament de vídeo,efectes gràfics,efectes d'escriptori,velocitat de les animacions +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,animationshastighed +X-KDE-Keywords[de]=kwin,Fenstermanager,Fensterverwaltung,Effekt,Fenster,Compositing,3D-Effekte,2D-Effekte,OpenGL,XRender,Video-Einstellungen,Grafikeffekte,Arbeitsflächeneffekte,Animationsgeschwindigkeit +X-KDE-Keywords[el]=kwin,παράθυρο,διαχειριστής,σύνθεση,εφέ,3D εφέ,2D εφέ,OpenGL,XRender,ρυθμίσεις βίντεο,γραφικά εφέ,εφέ επιφάνειας εργασίας,ταχύτητα κίνησης +X-KDE-Keywords[en_GB]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,animation speed +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,velocidad de animación +X-KDE-Keywords[et]=kwin,aken,haldur,komposiit,komposiitor,efekt,3D efektid,ruumilised efektid,2D efektid,OpenGL,XRender,videoseadistused,graafilised efektid,töölauaefektid,animatsiooni kiirus +X-KDE-Keywords[eu]=kwin,leiho,kudeatzaile,konposatzaile,efektu,3D efektuak,2D efektuak,OpenGL,XRender,bideo ezarpenak,efektu grafikoak,mahiagaineko efektuak,animazioaren abiadura +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,animointinopeus,animaationopeus +X-KDE-Keywords[fr]=kwin, fenêtre, gestionnaire, compositeur, effet, effets 3D, effets 2D, OpenGL, XRender, paramètres vidéo, effets graphiques, effets de bureau, vitesse d'animation +X-KDE-Keywords[gl]=kwin,xanela,xestor,composición,efecto,3D efectos,2D efectos,OpenGL,XRender,configuración da imaxe,efectos gráficos,efectos do escritorio,animation speed,velocidade das animacións +X-KDE-Keywords[hu]=kwin,ablak,kezelő,összeállítás,hatás,3D hatások,2D hatások,OpenGL,XRender,videobeállítások,grafikus hatások,asztali hatások,animációsebesség +X-KDE-Keywords[id]=kwin,jendela,pengelola,kompositing,efek,efek 3D,efek 2D,OpenGL,XRender,setelan video,efek grafis,efek desktop,kecepatan animasi +X-KDE-Keywords[it]=kwin,finestra,gestore,composizione,effetto,effetti 3D,effetti 2D,OpenGL,XRender,impostazioni video,effetti grafici,effetti del desktop,velocità delle animazioni +X-KDE-Keywords[ko]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,animation speed,3D 효과,2D 효과,비디오 설정,그래픽 설정,그래픽 효과,데스크톱 효과,애니메이션 속도 +X-KDE-Keywords[nl]=kwin,window,manager,beheerder,compositing,effecten,3D-effecten,2D-effecten,OpenGL,XRender,video-instellingen,grafische effecten,bureaubladeffecten,animatiesneleheid +X-KDE-Keywords[nn]=kwin,vindauge,vindaugshandsamar,samansetjing,effekt,3D-effektar,2D-effektar,OpenGL,XRender,videoinnstillingar,grafiske effektar,skrivebordseffektar,animasjonsfart +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,szybkość animacji +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ã,velocidade da animação +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,velocidade da animação +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,učinki namizja,hitrost animacije +X-KDE-Keywords[sr]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,animation speed,К‑вин,прозор,менаџер,слагање,ефекти,3Д ефекти,2Д ефекти,опенГЛ,Икс‑рендер,видео поставке,графички ефекти,ефекти површи,брзина анимације +X-KDE-Keywords[sr@ijekavian]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,animation speed,К‑вин,прозор,менаџер,слагање,ефекти,3Д ефекти,2Д ефекти,опенГЛ,Икс‑рендер,видео поставке,графички ефекти,ефекти површи,брзина анимације +X-KDE-Keywords[sr@ijekavianlatin]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,animation speed,KWin,prozor,menadžer,slaganje,efekti,3D efekti,2D efekti,OpenGL,XRender,video postavke,grafički efekti,efekti površi,brzina animacije +X-KDE-Keywords[sr@latin]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,animation speed,KWin,prozor,menadžer,slaganje,efekti,3D efekti,2D efekti,OpenGL,XRender,video postavke,grafički efekti,efekti površi,brzina animacije +X-KDE-Keywords[sv]=kwin,fönster,hantering,sammansättning,effekt,3D effekter,2D effekter,OpenGL,XRender,videoinställningar,grafiska effekter,skrivbordseffekter,animeringshastighet +X-KDE-Keywords[tr]=kwin,pencere,yönetici,birleştirme,efekt,3D efektler,2D efektler,OpenGL,XRender,görüntü ayarları,grafiksel efektler,masaüstü efektleri,animasyon hızı +X-KDE-Keywords[uk]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,animation speed,вікно,керування,композитне,композиція,ефект,просторовий,ефекти,плоскі,параметри відео,графічні ефекти,ефекти стільниці,швидкість анімації +X-KDE-Keywords[x-test]=xxkwinxx,xxwindowxx,xxmanagerxx,xxcompositingxx,xxeffectxx,xx3D effectsxx,xx2D effectsxx,xxOpenGLxx,xxXRenderxx,xxvideo settingsxx,xxgraphical effectsxx,xxdesktop effectsxx,xxanimation speedxx +X-KDE-Keywords[zh_CN]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,animation speed,窗口,管理器,混成,特效,3D 特效,2D 特效,视频设置,图形特效,桌面特效,动画速度 +X-KDE-Keywords[zh_TW]=kwin,window,manager,compositing,effect,3D effects,2D effects,OpenGL,XRender,video settings,graphical effects,desktop effects,animation speed diff --git a/kcmkwin/kwincompositing/kwineffect.knsrc b/kcmkwin/kwincompositing/kwineffect.knsrc new file mode 100644 index 0000000..d6b0e7c --- /dev/null +++ b/kcmkwin/kwincompositing/kwineffect.knsrc @@ -0,0 +1,46 @@ +[KNewStuff3] +Name=Window Manager Effects +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[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]=Window Manager Effects +Name[it]=Effetti del gestore delle finestre +Name[ko]=창 관리자 효과 +Name[lt]=Langų tvarkyklė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[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 +InstallationCommand=kpackagetool5 --type KWin/Effect --install %f +UninstallCommand=kpackagetool5 --type KWin/Effect --remove %f diff --git a/kcmkwin/kwincompositing/main.cpp b/kcmkwin/kwincompositing/main.cpp new file mode 100644 index 0000000..a7a28fc --- /dev/null +++ b/kcmkwin/kwincompositing/main.cpp @@ -0,0 +1,267 @@ +/************************************************************************** + * KWin - the KDE window manager * + * This file is part of the KDE project. * + * * + * Copyright (C) 2013 Antonis Tsiapaliokas * + * Copyright (C) 2013 Martin Gräßlin * + * * + * 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, see . * + **************************************************************************/ + + +#include "compositing.h" +#include "model.h" +#include "ui_compositing.h" +#include +#include +#include + +#include +#include + +class KWinCompositingKCM : public KCModule +{ + Q_OBJECT +public: + virtual ~KWinCompositingKCM(); + +public Q_SLOTS: + void save() override; + void load() override; + void defaults() override; + +protected: + explicit KWinCompositingKCM(QWidget* parent, const QVariantList& args, + KWin::Compositing::EffectView::ViewType viewType); + +private: + QScopedPointer m_view; +}; + +class KWinDesktopEffects : public KWinCompositingKCM +{ + Q_OBJECT +public: + explicit KWinDesktopEffects(QWidget* parent = 0, const QVariantList& args = QVariantList()) + : KWinCompositingKCM(parent, args, KWin::Compositing::EffectView::DesktopEffectsView) {} +}; + +class KWinCompositingSettings : public KCModule +{ + Q_OBJECT +public: + explicit KWinCompositingSettings(QWidget *parent = 0, const QVariantList &args = QVariantList()); + +public Q_SLOTS: + void load() override; + void save() override; + void defaults() override; + +private: + void init(); + KWin::Compositing::Compositing *m_compositing; + Ui_CompositingForm m_form; +}; + +KWinCompositingSettings::KWinCompositingSettings(QWidget *parent, const QVariantList &args) + : KCModule(parent, args) + , m_compositing(new KWin::Compositing::Compositing(this)) +{ + m_form.setupUi(this); + m_form.glCrashedWarning->setIcon(QIcon::fromTheme(QStringLiteral("dialog-warning"))); + QAction *reenableGLAction = new QAction(i18n("Re-enable OpenGL detection"), this); + connect(reenableGLAction, &QAction::triggered, m_compositing, &KWin::Compositing::Compositing::reenableOpenGLDetection); + 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.compositingEnabled->setVisible(!m_compositing->compositingRequired()); + m_form.windowsBlockCompositing->setVisible(!m_compositing->compositingRequired()); + + init(); +} + +void KWinCompositingSettings::init() +{ + using namespace KWin::Compositing; + auto currentIndexChangedSignal = static_cast(&QComboBox::currentIndexChanged); + + connect(m_compositing, &Compositing::changed, this, static_cast(&KWinCompositingSettings::changed)); + + // enabled check box + m_form.compositingEnabled->setChecked(m_compositing->compositingEnabled()); + connect(m_compositing, &Compositing::compositingEnabledChanged, m_form.compositingEnabled, &QCheckBox::setChecked); + connect(m_form.compositingEnabled, &QCheckBox::toggled, m_compositing, &Compositing::setCompositingEnabled); + + // animation speed + m_form.animationSpeed->setValue(m_compositing->animationSpeed()); + connect(m_compositing, &Compositing::animationSpeedChanged, m_form.animationSpeed, &QSlider::setValue); + connect(m_form.animationSpeed, &QSlider::valueChanged, m_compositing, &Compositing::setAnimationSpeed); + + // gl scale filter + m_form.glScaleFilter->setCurrentIndex(m_compositing->glScaleFilter()); + connect(m_compositing, &Compositing::glScaleFilterChanged, m_form.glScaleFilter, &QComboBox::setCurrentIndex); + connect(m_form.glScaleFilter, currentIndexChangedSignal, m_compositing, &Compositing::setGlScaleFilter); + connect(m_form.glScaleFilter, currentIndexChangedSignal, + [this](int index) { + if (index == 2) { + m_form.scaleWarning->animatedShow(); + } else { + m_form.scaleWarning->animatedHide(); + } + } + ); + + // xrender scale filter + m_form.xrScaleFilter->setCurrentIndex(m_compositing->xrScaleFilter()); + connect(m_compositing, &Compositing::xrScaleFilterChanged, m_form.xrScaleFilter, &QComboBox::setCurrentIndex); + connect(m_form.xrScaleFilter, currentIndexChangedSignal, m_compositing, &Compositing::setXrScaleFilter); + + // tearing prevention + m_form.tearingPrevention->setCurrentIndex(m_compositing->glSwapStrategy()); + connect(m_compositing, &Compositing::glSwapStrategyChanged, m_form.tearingPrevention, &QComboBox::setCurrentIndex); + connect(m_form.tearingPrevention, currentIndexChangedSignal, m_compositing, &Compositing::setGlSwapStrategy); + connect(m_form.tearingPrevention, currentIndexChangedSignal, + [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 + m_form.windowThumbnail->setCurrentIndex(m_compositing->windowThumbnail()); + connect(m_compositing, &Compositing::windowThumbnailChanged, m_form.windowThumbnail, &QComboBox::setCurrentIndex); + connect(m_form.windowThumbnail, currentIndexChangedSignal, m_compositing, &Compositing::setWindowThumbnail); + connect(m_form.windowThumbnail, currentIndexChangedSignal, + [this](int index) { + if (index == 2) { + m_form.windowThumbnailWarning->animatedShow(); + } else { + m_form.windowThumbnailWarning->animatedHide(); + } + } + ); + + // windows blocking compositing + m_form.windowsBlockCompositing->setChecked(m_compositing->windowsBlockCompositing()); + connect(m_compositing, &Compositing::windowsBlockCompositingChanged, m_form.windowsBlockCompositing, &QCheckBox::setChecked); + connect(m_form.windowsBlockCompositing, &QCheckBox::toggled, m_compositing, &Compositing::setWindowsBlockCompositing); + + // compositing type + CompositingType *type = new CompositingType(this); + m_form.type->setModel(type); + auto updateCompositingType = [this, type]() { + m_form.type->setCurrentIndex(type->indexForCompositingType(m_compositing->compositingType())); + }; + updateCompositingType(); + connect(m_compositing, &Compositing::compositingTypeChanged, + [updateCompositingType]() { + updateCompositingType(); + } + ); + auto showHideBasedOnType = [this, type]() { + const int currentType = type->compositingTypeForIndex(m_form.type->currentIndex()); + m_form.glScaleFilter->setVisible(currentType != CompositingType::XRENDER_INDEX); + m_form.glScaleFilterLabel->setVisible(currentType != CompositingType::XRENDER_INDEX); + m_form.xrScaleFilter->setVisible(currentType == CompositingType::XRENDER_INDEX); + m_form.xrScaleFilterLabel->setVisible(currentType == CompositingType::XRENDER_INDEX); + }; + showHideBasedOnType(); + connect(m_form.type, currentIndexChangedSignal, + [this, type, showHideBasedOnType]() { + m_compositing->setCompositingType(type->compositingTypeForIndex(m_form.type->currentIndex())); + showHideBasedOnType(); + } + ); + + if (m_compositing->OpenGLIsUnsafe()) { + m_form.glCrashedWarning->animatedShow(); + } +} + +void KWinCompositingSettings::load() +{ + KCModule::load(); + m_compositing->reset(); +} + +void KWinCompositingSettings::defaults() +{ + KCModule::defaults(); + m_compositing->defaults(); +} + +void KWinCompositingSettings::save() +{ + KCModule::save(); + m_compositing->save(); +} + +KWinCompositingKCM::KWinCompositingKCM(QWidget* parent, const QVariantList& args, KWin::Compositing::EffectView::ViewType viewType) + : KCModule(parent, args) + , m_view(new KWin::Compositing::EffectView(viewType)) +{ + QVBoxLayout *vl = new QVBoxLayout(this); + + vl->addWidget(m_view.data()); + setLayout(vl); + connect(m_view.data(), &KWin::Compositing::EffectView::changed, [this]{ + emit changed(true); + }); + m_view->setFocusPolicy(Qt::StrongFocus); +} + +KWinCompositingKCM::~KWinCompositingKCM() +{ +} + +void KWinCompositingKCM::save() +{ + m_view->save(); + KCModule::save(); +} + +void KWinCompositingKCM::load() +{ + m_view->load(); + KCModule::load(); +} + +void KWinCompositingKCM::defaults() +{ + m_view->defaults(); + KCModule::defaults(); +} + +K_PLUGIN_FACTORY(KWinCompositingConfigFactory, + registerPlugin("effects"); + registerPlugin("compositing"); + ) + +#include "main.moc" diff --git a/kcmkwin/kwincompositing/model.cpp b/kcmkwin/kwincompositing/model.cpp new file mode 100644 index 0000000..c48caf6 --- /dev/null +++ b/kcmkwin/kwincompositing/model.cpp @@ -0,0 +1,661 @@ +/************************************************************************** +* KWin - the KDE window manager * +* This file is part of the KDE project. * +* * +* Copyright (C) 2013 Antonis Tsiapaliokas * +* * +* 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, see . * +**************************************************************************/ + +#include "model.h" +#include "effectconfig.h" +#include "compositing.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 Compositing { + +static QString translatedCategory(const QString &category) +{ + static const QVector knownCategories = { + QStringLiteral("Accessibility"), + QStringLiteral("Appearance"), + QStringLiteral("Candy"), + QStringLiteral("Focus"), + QStringLiteral("Show Desktop Animation"), + QStringLiteral("Tools"), + QStringLiteral("Virtual Desktop Switching Animation"), + QStringLiteral("Window Management") + }; + + 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", "Candy"), + 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") + }; + const int index = knownCategories.indexOf(category); + if (index == -1) { + qDebug() << "Unknown category '" << category << "' and thus not translated"; + return category; + } + return translatedCategories[index]; +} + +static EffectStatus effectStatus(bool enabled) +{ + return enabled ? EffectStatus::Enabled : EffectStatus::Disabled; +} + +EffectModel::EffectModel(QObject *parent) + : QAbstractItemModel(parent) { +} + +QHash< int, QByteArray > EffectModel::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[EffectStatusRole] = "EffectStatusRole"; + roleNames[VideoRole] = "VideoRole"; + roleNames[SupportedRole] = "SupportedRole"; + roleNames[ExclusiveRole] = "ExclusiveRole"; + roleNames[ConfigurableRole] = "ConfigurableRole"; + roleNames[ScriptedRole] = QByteArrayLiteral("ScriptedRole"); + return roleNames; +} + +QModelIndex EffectModel::index(int row, int column, const QModelIndex &parent) const +{ +if (parent.isValid() || column > 0 || column < 0 || row < 0 || row >= m_effectsList.count()) { + return QModelIndex(); + } + + return createIndex(row, column); +} + +QModelIndex EffectModel::parent(const QModelIndex &child) const +{ + Q_UNUSED(child) + + return QModelIndex(); +} + +int EffectModel::columnCount(const QModelIndex &parent) const +{ + Q_UNUSED(parent) + return 1; +} + +int EffectModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + return m_effectsList.count(); +} + +QVariant EffectModel::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) { + return QVariant(); + } + + EffectData currentEffect = m_effectsList.at(index.row()); + switch (role) { + case Qt::DisplayRole: + case NameRole: + return m_effectsList.at(index.row()).name; + case DescriptionRole: + return m_effectsList.at(index.row()).description; + case AuthorNameRole: + return m_effectsList.at(index.row()).authorName; + case AuthorEmailRole: + return m_effectsList.at(index.row()).authorEmail; + case LicenseRole: + return m_effectsList.at(index.row()).license; + case VersionRole: + return m_effectsList.at(index.row()).version; + case CategoryRole: + return m_effectsList.at(index.row()).category; + case ServiceNameRole: + return m_effectsList.at(index.row()).serviceName; + case EffectStatusRole: + return (int)m_effectsList.at(index.row()).effectStatus; + case VideoRole: + return m_effectsList.at(index.row()).video; + case SupportedRole: + return m_effectsList.at(index.row()).supported; + case ExclusiveRole: + return m_effectsList.at(index.row()).exclusiveGroup; + case InternalRole: + return m_effectsList.at(index.row()).internal; + case ConfigurableRole: + return m_effectsList.at(index.row()).configurable; + case ScriptedRole: + return m_effectsList.at(index.row()).scripted; + default: + return QVariant(); + } +} + +bool EffectModel::setData(const QModelIndex& index, const QVariant& value, int role) +{ + if (!index.isValid()) + return QAbstractItemModel::setData(index, value, role); + + if (role == EffectModel::EffectStatusRole) { + // 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_effectsList[index.row()]; + data.effectStatus = EffectStatus(value.toInt()); + data.changed = true; + emit dataChanged(index, index); + + if (data.effectStatus == EffectStatus::Enabled && !data.exclusiveGroup.isEmpty()) { + // need to disable all other exclusive effects in the same category + for (int i = 0; i < m_effectsList.size(); ++i) { + if (i == index.row()) { + continue; + } + EffectData &otherData = m_effectsList[i]; + if (otherData.exclusiveGroup == data.exclusiveGroup) { + otherData.effectStatus = EffectStatus::Disabled; + otherData.changed = true; + emit dataChanged(this->index(i, 0), this->index(i, 0)); + } + } + } + + return true; + } + + return QAbstractItemModel::setData(index, value, role); +} + +void EffectModel::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.category = translatedCategory(data.category); + effect.serviceName = data.name; + effect.enabledByDefault = data.enabled; + effect.enabledByDefaultFunction = (data.enabledFunction != nullptr); + const QString enabledKey = QStringLiteral("%1Enabled").arg(effect.serviceName); + if (kwinConfig.hasKey(enabledKey)) { + effect.effectStatus = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", effect.enabledByDefault)); + } else if (data.enabledFunction != nullptr) { + effect.effectStatus = EffectStatus::EnabledUndeterminded; + } else { + effect.effectStatus = effectStatus(effect.enabledByDefault); + } + effect.video = data.video; + effect.supported = true; + effect.exclusiveGroup = data.exclusiveCategory; + effect.internal = data.internal; + effect.scripted = false; + + auto it = std::find_if(configs.begin(), configs.end(), [data](const KPluginInfo &info) { + return info.property(QStringLiteral("X-KDE-ParentComponents")).toString() == data.name; + }); + effect.configurable = it != configs.end(); + + m_effectsList << effect; + } +} + +void EffectModel::loadJavascriptEffects(const KConfigGroup &kwinConfig) +{ + KService::List offers = KServiceTypeTrader::self()->query("KWin/Effect", QStringLiteral("[X-Plasma-API] == 'javascript'")); + for(KService::Ptr service : offers) { + const QString effectPluginPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, "kservices5/"+ service->entryPath(), QStandardPaths::LocateFile); + KPluginInfo plugin(effectPluginPath); + 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.category = translatedCategory(plugin.category()); + effect.serviceName = plugin.pluginName(); + effect.effectStatus = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", plugin.isPluginEnabledByDefault())); + effect.enabledByDefault = plugin.isPluginEnabledByDefault(); + effect.enabledByDefaultFunction = false; + effect.video = service->property(QStringLiteral("X-KWin-Video-Url"), QVariant::Url).toUrl(); + effect.supported = true; + effect.exclusiveGroup = service->property(QStringLiteral("X-KWin-Exclusive-Category"), QVariant::String).toString(); + effect.internal = service->property(QStringLiteral("X-KWin-Internal"), QVariant::Bool).toBool(); + effect.scripted = true; + + if (!service->pluginKeyword().isEmpty()) { + // scripted effects have their pluginName() as the keyword + effect.configurable = service->property(QStringLiteral("X-KDE-ParentComponents")).toString() == service->pluginKeyword(); + } else { + effect.configurable = false; + } + + m_effectsList << effect; + } +} + +void EffectModel::loadPluginEffects(const KConfigGroup &kwinConfig, const KPluginInfo::List &configs) +{ + static const QString subDir(QStringLiteral("kwin/effects/plugins/")); + static const QString serviceType(QStringLiteral("KWin/Effect")); + const QVector pluginEffects = KPluginLoader::findPlugins(subDir, [] (const KPluginMetaData &data) { return data.serviceTypes().contains(serviceType); }); + for (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.category = pluginEffect.category(); + effect.serviceName = pluginEffect.pluginId(); + effect.enabledByDefault = pluginEffect.isEnabledByDefault(); + effect.supported = true; + effect.enabledByDefaultFunction = false; + effect.internal = false; + effect.scripted = false; + + 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(); + } + + const QString enabledKey = QStringLiteral("%1Enabled").arg(effect.serviceName); + if (kwinConfig.hasKey(enabledKey)) { + effect.effectStatus = effectStatus(kwinConfig.readEntry(effect.serviceName + "Enabled", effect.enabledByDefault)); + } else if (effect.enabledByDefaultFunction) { + effect.effectStatus = EffectStatus::EnabledUndeterminded; + } else { + effect.effectStatus = effectStatus(effect.enabledByDefault); + } + + auto it = std::find_if(configs.begin(), configs.end(), [pluginEffect](const KPluginInfo &info) { + return info.property(QStringLiteral("X-KDE-ParentComponents")).toString() == pluginEffect.pluginId(); + }); + effect.configurable = it != configs.end(); + + m_effectsList << effect; + } +} + +void EffectModel::loadEffects() +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), "Plugins"); + + beginResetModel(); + m_effectsChanged.clear(); + m_effectsList.clear(); + const KPluginInfo::List configs = KPluginTrader::self()->query(QStringLiteral("kwin/effects/configs/")); + loadBuiltInEffects(kwinConfig, configs); + loadJavascriptEffects(kwinConfig); + loadPluginEffects(kwinConfig, configs); + + qSort(m_effectsList.begin(), m_effectsList.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; + }); + + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + if (interface.isValid()) { + QStringList effectNames; + std::for_each(m_effectsList.constBegin(), m_effectsList.constEnd(), [&effectNames](const EffectData &data) { + effectNames << data.serviceName; + }); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(interface.areEffectsSupported(effectNames), this); + watcher->setProperty("effectNames", effectNames); + connect(watcher, &QDBusPendingCallWatcher::finished, [this](QDBusPendingCallWatcher *self) { + const QStringList effectNames = self->property("effectNames").toStringList(); + const QDBusPendingReply< QList< bool > > reply = *self; + QList< bool> supportValues; + if (reply.isValid()) { + supportValues.append(reply.value()); + } + if (effectNames.size() == supportValues.size()) { + for (int i = 0; i < effectNames.size(); ++i) { + const bool supportedValue = supportValues.at(i); + const QString &effectName = effectNames.at(i); + auto it = std::find_if(m_effectsList.begin(), m_effectsList.end(), [effectName](const EffectData &data) { + return data.serviceName == effectName; + }); + if (it != m_effectsList.end()) { + if ((*it).supported != supportedValue) { + (*it).supported = supportedValue; + QModelIndex i = index(findRowByServiceName(effectName), 0); + if (i.isValid()) { + emit dataChanged(i, i, QVector() << SupportedRole); + } + } + } + } + } + self->deleteLater(); + }); + } + + m_effectsChanged = m_effectsList; + endResetModel(); +} + +int EffectModel::findRowByServiceName(const QString &serviceName) +{ + for (int it = 0; it < m_effectsList.size(); it++) { + if (m_effectsList.at(it).serviceName == serviceName) { + return it; + } + } + return -1; +} + +void EffectModel::syncEffectsToKWin() +{ + OrgKdeKwinEffectsInterface interface(QStringLiteral("org.kde.KWin"), + QStringLiteral("/Effects"), + QDBusConnection::sessionBus()); + for (int it = 0; it < m_effectsList.size(); it++) { + if (m_effectsList.at(it).effectStatus != m_effectsChanged.at(it).effectStatus) { + if (m_effectsList.at(it).effectStatus != EffectStatus::Disabled) { + interface.loadEffect(m_effectsList.at(it).serviceName); + } else { + interface.unloadEffect(m_effectsList.at(it).serviceName); + } + } + } + + m_effectsChanged = m_effectsList; +} + +void EffectModel::updateEffectStatus(const QModelIndex &rowIndex, EffectStatus effectState) +{ + setData(rowIndex, (int)effectState, EffectModel::EffectStatusRole); +} + +void EffectModel::syncConfig() +{ + KConfigGroup kwinConfig(KSharedConfig::openConfig("kwinrc"), "Plugins"); + + for (auto it = m_effectsList.begin(); it != m_effectsList.end(); it++) { + EffectData &effect = *(it); + if (!effect.changed) { + continue; + } + effect.changed = false; + + const QString key = effect.serviceName + QStringLiteral("Enabled"); + const bool shouldEnable = (effect.effectStatus != EffectStatus::Disabled); + const bool restoreToDefault = effect.enabledByDefaultFunction + ? effect.effectStatus == EffectStatus::EnabledUndeterminded + : shouldEnable == effect.enabledByDefault; + if (restoreToDefault) { + kwinConfig.deleteEntry(key); + } else { + kwinConfig.writeEntry(key, shouldEnable); + } + } + + kwinConfig.sync(); + syncEffectsToKWin(); +} + +void EffectModel::defaults() +{ + for (int i = 0; i < m_effectsList.count(); ++i) { + const auto &effect = m_effectsList.at(i); + if (effect.enabledByDefaultFunction && effect.effectStatus != EffectStatus::EnabledUndeterminded) { + updateEffectStatus(index(i, 0), EffectStatus::EnabledUndeterminded); + } else if ((bool)effect.effectStatus != effect.enabledByDefault) { + updateEffectStatus(index(i, 0), effect.enabledByDefault ? EffectStatus::Enabled : EffectStatus::Disabled); + } + } +} + +EffectFilterModel::EffectFilterModel(QObject *parent) + : QSortFilterProxyModel(parent) + , m_effectModel(new EffectModel(this)) + , m_filterOutUnsupported(true) + , m_filterOutInternal(true) +{ + setSourceModel(m_effectModel); + connect(this, &EffectFilterModel::filterOutUnsupportedChanged, this, &EffectFilterModel::invalidateFilter); + connect(this, &EffectFilterModel::filterOutInternalChanged, this, &EffectFilterModel::invalidateFilter); +} + +const QString &EffectFilterModel::filter() const +{ + return m_filter; +} + +void EffectFilterModel::setFilter(const QString &filter) +{ + if (filter == m_filter) { + return; + } + + m_filter = filter; + emit filterChanged(); + invalidateFilter(); +} + +bool EffectFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const +{ + if (!m_effectModel) { + return false; + } + + QModelIndex index = m_effectModel->index(source_row, 0, source_parent); + if (!index.isValid()) { + return false; + } + + if (m_filterOutUnsupported) { + if (!index.data(EffectModel::SupportedRole).toBool()) { + return false; + } + } + + if (m_filterOutInternal) { + if (index.data(EffectModel::InternalRole).toBool()) { + return false; + } + } + + if (m_filter.isEmpty()) { + return true; + } + + QVariant data = index.data(); + if (!data.isValid()) { + //An invalid QVariant is valid data + return true; + } + + if (m_effectModel->data(index, EffectModel::NameRole).toString().contains(m_filter, Qt::CaseInsensitive)) { + return true; + } else if (m_effectModel->data(index, EffectModel::DescriptionRole).toString().contains(m_filter, Qt::CaseInsensitive)) { + return true; + } + if (index.data(EffectModel::CategoryRole).toString().contains(m_filter, Qt::CaseInsensitive)) { + return true; + } + + return false; +} + +void EffectFilterModel::updateEffectStatus(int rowIndex, int effectState) +{ + const QModelIndex sourceIndex = mapToSource(index(rowIndex, 0)); + + m_effectModel->updateEffectStatus(sourceIndex, EffectStatus(effectState)); +} + +void EffectFilterModel::syncConfig() +{ + m_effectModel->syncConfig(); +} + +void EffectFilterModel::load() +{ + m_effectModel->loadEffects(); +} + +void EffectFilterModel::defaults() +{ + m_effectModel->defaults(); +} + +EffectView::EffectView(ViewType type, QWidget *parent) + : QQuickWidget(parent) +{ + qRegisterMetaType(); + qmlRegisterType("org.kde.kwin.kwincompositing", 1, 0, "EffectConfig"); + qmlRegisterType("org.kde.kwin.kwincompositing", 1, 0, "EffectFilterModel"); + qmlRegisterType("org.kde.kwin.kwincompositing", 1, 0, "Compositing"); + qmlRegisterType("org.kde.kwin.kwincompositing", 1, 0, "CompositingType"); + init(type); +} + +void EffectView::init(ViewType type) +{ + KDeclarative::KDeclarative kdeclarative; + kdeclarative.setDeclarativeEngine(engine()); + kdeclarative.setTranslationDomain(QStringLiteral(TRANSLATION_DOMAIN)); + kdeclarative.setupContext(); + kdeclarative.setupEngine(engine()); + QString path; + switch (type) { + case CompositingSettingsView: + path = QStringLiteral("kwincompositing/qml/main-compositing.qml"); + break; + case DesktopEffectsView: + path = QStringLiteral("kwincompositing/qml/main.qml"); + break; + } + QString mainFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, path, QStandardPaths::LocateFile); + setResizeMode(QQuickWidget::SizeRootObjectToView); + setSource(QUrl(mainFile)); + rootObject()->setProperty("color", + KColorScheme(QPalette::Active, KColorScheme::Window, KSharedConfigPtr(0)).background(KColorScheme::NormalBackground).color()); + connect(rootObject(), SIGNAL(changed()), this, SIGNAL(changed())); + setMinimumSize(initialSize()); + connect(rootObject(), SIGNAL(implicitWidthChanged()), this, SLOT(slotImplicitSizeChanged())); + connect(rootObject(), SIGNAL(implicitHeightChanged()), this, SLOT(slotImplicitSizeChanged())); +} + +void EffectView::save() +{ + if (auto *model = rootObject()->findChild(QStringLiteral("filterModel"))) { + model->syncConfig(); + } + if (auto *compositing = rootObject()->findChild(QStringLiteral("compositing"))) { + compositing->save(); + } +} + +void EffectView::load() +{ + if (auto *model = rootObject()->findChild(QStringLiteral("filterModel"))) { + model->load(); + } + if (auto *compositing = rootObject()->findChild(QStringLiteral("compositing"))) { + compositing->reset(); + } +} + +void EffectView::defaults() +{ + if (auto *model = rootObject()->findChild(QStringLiteral("filterModel"))) { + model->defaults(); + } + if (auto *compositing = rootObject()->findChild(QStringLiteral("compositing"))) { + compositing->defaults(); + } +} + +void EffectView::slotImplicitSizeChanged() +{ + setMinimumSize(QSize(rootObject()->property("implicitWidth").toInt(), + rootObject()->property("implicitHeight").toInt())); +} + +}//end namespace Compositing +}//end namespace KWin diff --git a/kcmkwin/kwincompositing/model.h b/kcmkwin/kwincompositing/model.h new file mode 100644 index 0000000..8adde91 --- /dev/null +++ b/kcmkwin/kwincompositing/model.h @@ -0,0 +1,201 @@ +/************************************************************************** + * KWin - the KDE window manager * + * This file is part of the KDE project. * + * * + * Copyright (C) 2013 Antonis Tsiapaliokas * + * * + * 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, see . * + **************************************************************************/ + + +#ifndef MODEL_H +#define MODEL_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin { +namespace Compositing { + +enum class EffectStatus { + Disabled = Qt::Unchecked, + EnabledUndeterminded = Qt::PartiallyChecked, + Enabled = Qt::Checked +}; + +struct EffectData { + QString name; + QString description; + QString authorName; + QString authorEmail; + QString license; + QString version; + QString category; + QString serviceName; + EffectStatus effectStatus; + bool enabledByDefault; + bool enabledByDefaultFunction; + QUrl video; + bool supported; + QString exclusiveGroup; + bool internal; + bool configurable; + bool scripted; + bool changed = false; +}; + +class EffectModel : public QAbstractItemModel +{ + + Q_OBJECT + +public: + enum EffectRoles { + NameRole = Qt::UserRole + 1, + DescriptionRole, + AuthorNameRole, + AuthorEmailRole, + LicenseRole, + VersionRole, + CategoryRole, + ServiceNameRole, + EffectStatusRole, + VideoRole, + SupportedRole, + ExclusiveRole, + InternalRole, + ConfigurableRole, + ScriptedRole + }; + + explicit EffectModel(QObject *parent = 0); + + QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; + QModelIndex parent(const QModelIndex &child) const override; + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(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 = Qt::EditRole) override; + QString serviceName(const QString &effectName); + + virtual QHash< int, QByteArray > roleNames() const override; + + void updateEffectStatus(const QModelIndex &rowIndex, EffectStatus effectState); + void syncEffectsToKWin(); + void syncConfig(); + void loadEffects(); + void defaults(); + +private: + void loadBuiltInEffects(const KConfigGroup &kwinConfig, const KPluginInfo::List &configs); + void loadJavascriptEffects(const KConfigGroup &kwinConfig); + void loadPluginEffects(const KConfigGroup &kwinConfig, const KPluginInfo::List &configs); + int findRowByServiceName(const QString &serviceName); + QList m_effectsList; + QList m_effectsChanged; + +}; + +class EffectView : public QQuickWidget +{ + + Q_OBJECT + +public: + enum ViewType { + DesktopEffectsView, + CompositingSettingsView + }; + EffectView(ViewType type, QWidget *parent = 0); + + void save(); + void load(); + void defaults(); + +Q_SIGNALS: + void changed(); + +private Q_SLOTS: + void slotImplicitSizeChanged(); +private: + void init(ViewType type); +}; + + +class EffectFilterModel : public QSortFilterProxyModel +{ + Q_OBJECT + Q_PROPERTY(QString filter READ filter WRITE setFilter NOTIFY filterChanged) + /** + * If @c true not supported effects are excluded, if @c false no restriction on supported. + * Default value is @c true. + **/ + Q_PROPERTY(bool filterOutUnsupported MEMBER m_filterOutUnsupported NOTIFY filterOutUnsupportedChanged) + /** + * If @c true internal effects are excluded, if @c false no restriction on internal. + * Default value is @c true. + **/ + Q_PROPERTY(bool filterOutInternal MEMBER m_filterOutInternal NOTIFY filterOutInternalChanged) + Q_PROPERTY(QColor backgroundActiveColor READ backgroundActiveColor CONSTANT) + Q_PROPERTY(QColor backgroundNormalColor READ backgroundNormalColor CONSTANT) + Q_PROPERTY(QColor backgroundAlternateColor READ backgroundAlternateColor CONSTANT) + Q_PROPERTY(QColor sectionColor READ sectionColor CONSTANT) +public: + EffectFilterModel(QObject *parent = 0); + const QString &filter() const; + + Q_INVOKABLE void updateEffectStatus(int rowIndex, int effectState); + Q_INVOKABLE void syncConfig(); + Q_INVOKABLE void load(); + + QColor backgroundActiveColor() { return KColorScheme(QPalette::Active, KColorScheme::Selection, KSharedConfigPtr(0)).background(KColorScheme::LinkBackground).color(); }; + QColor backgroundNormalColor() { return KColorScheme(QPalette::Active, KColorScheme::View, KSharedConfigPtr(0)).background(KColorScheme::NormalBackground).color(); }; + QColor backgroundAlternateColor() { return KColorScheme(QPalette::Active, KColorScheme::View, KSharedConfigPtr(0)).background(KColorScheme::AlternateBackground).color(); }; + + QColor sectionColor() const { + QColor color = KColorScheme(QPalette::Active).foreground().color(); + color.setAlphaF(0.6); + return color; + } + + void defaults(); + +public Q_SLOTS: + void setFilter(const QString &filter); + +protected: + virtual bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const; + +Q_SIGNALS: + void effectModelChanged(); + void filterChanged(); + void filterOutUnsupportedChanged(); + void filterOutInternalChanged(); + +private: + EffectModel *m_effectModel; + QString m_filter; + bool m_filterOutUnsupported; + bool m_filterOutInternal; +}; +} +} +#endif diff --git a/kcmkwin/kwincompositing/qml/Effect.qml b/kcmkwin/kwincompositing/qml/Effect.qml new file mode 100644 index 0000000..4b03832 --- /dev/null +++ b/kcmkwin/kwincompositing/qml/Effect.qml @@ -0,0 +1,170 @@ +/************************************************************************** +* KWin - the KDE window manager * +* This file is part of the KDE project. * +* * +* Copyright (C) 2013 Antonis Tsiapaliokas * +* * +* 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, see . * +**************************************************************************/ + + +import QtQuick 2.1 +import QtQuick.Controls 1.1 +import QtQuick.Controls 2.0 as QQC2 +import QtQuick.Layouts 1.0 +import org.kde.kwin.kwincompositing 1.0 + +Rectangle { + id: item + width: parent.width + height: rowEffect.implicitHeight + color: item.ListView.isCurrentItem ? effectView.backgroundActiveColor : index % 2 ? effectView.backgroundNormalColor : effectView.backgroundAlternateColor + signal changed() + property int checkedState: model.EffectStatusRole + + MouseArea { + anchors.fill: parent + onClicked: { + effectView.currentIndex = index; + } + } + + RowLayout { + id: rowEffect + property int maximumWidth: parent.width - 2 * spacing + width: maximumWidth + Layout.maximumWidth: maximumWidth + x: spacing + + RowLayout { + id: checkBoxLayout + RadioButton { + id: exclusiveGroupButton + property bool exclusive: model.ExclusiveRole != "" + visible: exclusive + checked: model.EffectStatusRole + property bool actuallyChanged: true + property bool initiallyChecked: false + exclusiveGroup: exclusive ? effectView.exclusiveGroupForCategory(model.ExclusiveRole) : null + onCheckedChanged: { + if (!visible) { + return; + } + actuallyChanged = true; + item.checkedState = exclusiveGroupButton.checked ? Qt.Checked : Qt.Unchecked + item.changed(); + } + onClicked: { + if (!actuallyChanged || initiallyChecked) { + checked = false; + } + actuallyChanged = false; + initiallyChecked = false; + } + Component.onCompleted: { + exclusiveGroupButton.initiallyChecked = model.EffectStatusRole; + } + } + + CheckBox { + id: effectStatusCheckBox + checkedState: model.EffectStatusRole + visible: model.ExclusiveRole == "" + + onCheckedStateChanged: { + if (!visible) { + return; + } + item.checkedState = effectStatusCheckBox.checkedState; + item.changed(); + } + Connections { + target: searchModel + onDataChanged: { + effectStatusCheckBox.checkedState = model.EffectStatusRole; + } + } + } + } + + ColumnLayout { + id: effectItem + property int maximumWidth: parent.maximumWidth - checkBoxLayout.width - (videoButton.width + configureButton.width + aboutButton.width) - parent.spacing * 5 + Layout.maximumWidth: maximumWidth + QQC2.Label { + text: model.NameRole + font.weight: Font.Bold + wrapMode: Text.Wrap + Layout.maximumWidth: parent.maximumWidth + } + QQC2.Label { + id: desc + text: model.DescriptionRole + wrapMode: Text.Wrap + Layout.maximumWidth: parent.maximumWidth + } + QQC2.Label { + id:aboutItem + text: i18n("Author: %1\nLicense: %2", model.AuthorNameRole, model.LicenseRole) + font.weight: Font.Bold + visible: false + wrapMode: Text.Wrap + Layout.maximumWidth: parent.maximumWidth + } + Loader { + id: videoItem + active: false + source: "Video.qml" + function showHide() { + if (!videoItem.active) { + videoItem.active = true; + } else { + videoItem.item.showHide(); + } + } + onLoaded: { + videoItem.item.showHide(); + } + } + } + Item { + // spacer + Layout.fillWidth: true + } + + Button { + id: videoButton + visible: model.VideoRole.toString() !== "" + iconName: "video" + onClicked: videoItem.showHide() + } + Button { + id: configureButton + visible: ConfigurableRole + enabled: item.checkedState != Qt.Unchecked + iconName: "configure" + onClicked: { + effectConfig.openConfig(model.ServiceNameRole, model.ScriptedRole, model.NameRole); + } + } + + Button { + id: aboutButton + iconName: "dialog-information" + onClicked: { + aboutItem.visible = !aboutItem.visible; + } + } + } //end Row +} //end Rectangle diff --git a/kcmkwin/kwincompositing/qml/EffectView.qml b/kcmkwin/kwincompositing/qml/EffectView.qml new file mode 100644 index 0000000..fe1f917 --- /dev/null +++ b/kcmkwin/kwincompositing/qml/EffectView.qml @@ -0,0 +1,177 @@ +/************************************************************************** +* KWin - the KDE window manager * +* This file is part of the KDE project. * +* * +* Copyright (C) 2013 Antonis Tsiapaliokas * +* * +* 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, see . * +**************************************************************************/ + +import QtQuick 2.1 +import QtQuick.Controls 1.0 +import QtQuick.Controls 2.0 as QQC2 +import QtQuick.Layouts 1.0 +import org.kde.kwin.kwincompositing 1.0 + +Rectangle { + signal changed + implicitWidth: col.implicitWidth + implicitHeight: col.implicitHeight + + Component { + id: sectionHeading + Rectangle { + width: parent.width + implicitHeight: sectionText.implicitHeight + 2 * col.spacing + color: searchModel.backgroundNormalColor + + QQC2.Label { + id: sectionText + x: col.spacing + y: col.spacing + text: section + font.weight: Font.Bold + color: searchModel.sectionColor + anchors.horizontalCenter: parent.horizontalCenter + } + } + } + + EffectConfig { + id: effectConfig + onEffectListChanged: { + searchModel.load() + } + } + + ColumnLayout { + id: col + anchors.fill: parent + + QQC2.Label { + id: hint + text: i18n("Hint: To find out or configure how to activate an effect, look at the effect's settings.") + anchors { + top: parent.top + left: parent.left + } + } + + RowLayout { + QQC2.TextField { + // TODO: needs clear button, missing in Qt + id: searchField + placeholderText: i18n("Search") + Layout.fillWidth: true + focus: true + } + + Button { + iconName: "configure" + tooltip: i18n("Configure filter") + menu: Menu { + MenuItem { + text: i18n("Exclude Desktop Effects not supported by the Compositor") + checkable: true + checked: searchModel.filterOutUnsupported + onTriggered: { + searchModel.filterOutUnsupported = !searchModel.filterOutUnsupported; + } + } + MenuItem { + text: i18n("Exclude internal Desktop Effects") + checkable: true + checked: searchModel.filterOutInternal + onTriggered: { + searchModel.filterOutInternal = !searchModel.filterOutInternal + } + } + } + } + + Button { + id: ghnsButton + text: i18n("Get New Effects...") + iconName: "get-hot-new-stuff" + onClicked: effectConfig.openGHNS() + } + } + + EffectFilterModel { + id: searchModel + objectName: "filterModel" + filter: searchField.text + } + + ScrollView { + id: scroll + frameVisible: true + highlightOnFocus: true + Layout.fillWidth: true + Layout.fillHeight: true + Rectangle { + color: effectView.backgroundNormalColor + anchors.fill: parent + } + ListView { + function exclusiveGroupForCategory(category) { + for (var i = 0; i < effectView.exclusiveGroups.length; ++i) { + var item = effectView.exclusiveGroups[i]; + if (item.category == category) { + return item.group; + } + } + var newGroup = Qt.createQmlObject('import QtQuick 2.1; import QtQuick.Controls 1.1; ExclusiveGroup {}', + effectView, + "dynamicExclusiveGroup" + effectView.exclusiveGroups.length); + effectView.exclusiveGroups[effectView.exclusiveGroups.length] = { + 'category': category, + 'group': newGroup + }; + return newGroup; + } + id: effectView + property var exclusiveGroups: [] + property color backgroundActiveColor: searchModel.backgroundActiveColor + property color backgroundNormalColor: searchModel.backgroundNormalColor + property color backgroundAlternateColor: searchModel.backgroundAlternateColor + anchors.fill: parent + model: searchModel + delegate: Effect{ + id: effectDelegate + Connections { + id: effectStateConnection + target: null + onChanged: { + searchModel.updateEffectStatus(index, checkedState); + } + } + Component.onCompleted: { + effectStateConnection.target = effectDelegate + } + } + + section.property: "CategoryRole" + section.delegate: sectionHeading + spacing: col.spacing + focus: true + } + } + + }//End ColumnLayout + Connections { + target: searchModel + onDataChanged: changed() + } +}//End item diff --git a/kcmkwin/kwincompositing/qml/Video.qml b/kcmkwin/kwincompositing/qml/Video.qml new file mode 100644 index 0000000..17c4b8d --- /dev/null +++ b/kcmkwin/kwincompositing/qml/Video.qml @@ -0,0 +1,72 @@ +/************************************************************************** +* KWin - the KDE window manager * +* This file is part of the KDE project. * +* * +* Copyright (C) 2013 Antonis Tsiapaliokas * +* Copyright (C) 2014 Martin Gräßlin * +* * +* 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, see . * +**************************************************************************/ +import QtQuick 2.1 +import QtQuick.Controls 1.1 +import QtQuick.Layouts 1.0 +import QtMultimedia 5.0 as Multimedia +import org.kde.kquickcontrolsaddons 2.0 as QtExtra + +Multimedia.Video { + id: videoItem + function showHide() { + replayButton.visible = false; + if (videoItem.visible === true) { + videoItem.stop(); + videoItem.visible = false; + } else { + videoItem.visible = true; + videoItem.play(); + } + } + autoLoad: false + visible: false + source: model.VideoRole + width: 400 + height: 400 + BusyIndicator { + anchors.centerIn: parent + visible: videoItem.status == Multimedia.MediaPlayer.Loading + running: true + } + MouseArea { + // it's a mouse area with icon inside to not have an ugly button background + id: replayButton + visible: false + anchors.fill: parent + onClicked: { + replayButton.visible = false; + videoItem.play(); + } + QtExtra.QIconItem { + id: replayIcon + anchors.centerIn: parent + width: 16 + height: 16 + icon: "media-playback-start" + } + Connections { + target: videoItem + onStopped: { + replayButton.visible = true + } + } + } +} diff --git a/kcmkwin/kwincompositing/qml/main.qml b/kcmkwin/kwincompositing/qml/main.qml new file mode 100644 index 0000000..b4ff1a3 --- /dev/null +++ b/kcmkwin/kwincompositing/qml/main.qml @@ -0,0 +1,31 @@ +/************************************************************************** +* KWin - the KDE window manager * +* This file is part of the KDE project. * +* * +* Copyright (C) 2013 Antonis Tsiapaliokas * +* * +* 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, see . * +**************************************************************************/ + +import QtQuick 2.1 +import QtQuick.Controls 1.0 +import QtQuick.Layouts 1.0 +import org.kde.kwin.kwincompositing 1.0 + +EffectView{ + id: view + onChanged: { + window.changed() + } +} diff --git a/kcmkwin/kwincompositing/test/effectmodeltest.cpp b/kcmkwin/kwincompositing/test/effectmodeltest.cpp new file mode 100644 index 0000000..3b49c04 --- /dev/null +++ b/kcmkwin/kwincompositing/test/effectmodeltest.cpp @@ -0,0 +1,43 @@ +/************************************************************************** +* KWin - the KDE window manager * +* This file is part of the KDE project. * +* * +* Copyright (C) 2013 Antonis Tsiapaliokas * +* * +* 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, see . * +**************************************************************************/ + +#include "modeltest.h" +#include "../model.h" +#include "effectmodeltest.h" +#include + +EffectModelTest::EffectModelTest(QObject *parent) + : QObject(parent) { + +} + +void EffectModelTest::testEffectModel() { + KWin::Compositing::EffectModel *effectModel = new KWin::Compositing::EffectModel(); + + new ModelTest(effectModel, this); +} + +void EffectModelTest::testEffectFilterModel() { + KWin::Compositing::EffectFilterModel *model = new KWin::Compositing::EffectFilterModel(); + + new ModelTest(model, this); +} + +QTEST_MAIN(EffectModelTest) diff --git a/kcmkwin/kwincompositing/test/effectmodeltest.h b/kcmkwin/kwincompositing/test/effectmodeltest.h new file mode 100644 index 0000000..cb71a76 --- /dev/null +++ b/kcmkwin/kwincompositing/test/effectmodeltest.h @@ -0,0 +1,38 @@ +/************************************************************************** + * KWin - the KDE window manager * + * This file is part of the KDE project. * + * * + * Copyright (C) 2013 Antonis Tsiapaliokas * + * * + * 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, see . * + **************************************************************************/ + + +#ifndef EFFECTMODELTEST_H +#define EFFECTMODELTEST_H + +#include + +class EffectModelTest : public QObject { + + Q_OBJECT + +public: + EffectModelTest(QObject *parent = 0); + +private Q_SLOTS: + void testEffectModel(); + void testEffectFilterModel(); +}; +#endif diff --git a/kcmkwin/kwincompositing/test/modeltest.cpp b/kcmkwin/kwincompositing/test/modeltest.cpp new file mode 100644 index 0000000..66fe057 --- /dev/null +++ b/kcmkwin/kwincompositing/test/modeltest.cpp @@ -0,0 +1,600 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#include + +#include "modeltest.h" + +#include + +/*! + Connect to all of the models signals. Whenever anything happens recheck everything. +*/ +ModelTest::ModelTest ( QAbstractItemModel *_model, QObject *parent ) : QObject ( parent ), model ( _model ), fetchingMore ( false ) +{ + if (!model) + qFatal("%s: model must not be null", Q_FUNC_INFO); + + connect(model, SIGNAL(columnsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(columnsRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(layoutAboutToBeChanged()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(layoutChanged()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(modelReset()), this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(runAllTests()) ); + + // Special checks for changes + connect(model, SIGNAL(layoutAboutToBeChanged()), + this, SLOT(layoutAboutToBeChanged()) ); + connect(model, SIGNAL(layoutChanged()), + this, SLOT(layoutChanged()) ); + + connect(model, SIGNAL(rowsAboutToBeInserted(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeInserted(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsAboutToBeRemoved(QModelIndex,int,int)), + this, SLOT(rowsAboutToBeRemoved(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsInserted(QModelIndex,int,int)), + this, SLOT(rowsInserted(QModelIndex,int,int)) ); + connect(model, SIGNAL(rowsRemoved(QModelIndex,int,int)), + this, SLOT(rowsRemoved(QModelIndex,int,int)) ); + connect(model, SIGNAL(dataChanged(QModelIndex,QModelIndex)), + this, SLOT(dataChanged(QModelIndex,QModelIndex)) ); + connect(model, SIGNAL(headerDataChanged(Qt::Orientation,int,int)), + this, SLOT(headerDataChanged(Qt::Orientation,int,int)) ); + + runAllTests(); +} + +void ModelTest::runAllTests() +{ + if ( fetchingMore ) + return; + nonDestructiveBasicTest(); + rowCount(); + columnCount(); + hasIndex(); + index(); + parent(); + data(); +} + +/*! + nonDestructiveBasicTest tries to call a number of the basic functions (not all) + to make sure the model doesn't outright segfault, testing the functions that makes sense. +*/ +void ModelTest::nonDestructiveBasicTest() +{ + QVERIFY( model->buddy ( QModelIndex() ) == QModelIndex() ); + model->canFetchMore ( QModelIndex() ); + QVERIFY( model->columnCount ( QModelIndex() ) >= 0 ); + QVERIFY( model->data ( QModelIndex() ) == QVariant() ); + fetchingMore = true; + model->fetchMore ( QModelIndex() ); + fetchingMore = false; + Qt::ItemFlags flags = model->flags ( QModelIndex() ); + QVERIFY( flags == Qt::ItemIsDropEnabled || flags == 0 ); + model->hasChildren ( QModelIndex() ); + model->hasIndex ( 0, 0 ); + model->headerData ( 0, Qt::Horizontal ); + model->index ( 0, 0 ); + model->itemData ( QModelIndex() ); + QVariant cache; + model->match ( QModelIndex(), -1, cache ); + model->mimeTypes(); + QVERIFY( model->parent ( QModelIndex() ) == QModelIndex() ); + QVERIFY( model->rowCount() >= 0 ); + QVariant variant; + model->setData ( QModelIndex(), variant, -1 ); + model->setHeaderData ( -1, Qt::Horizontal, QVariant() ); + model->setHeaderData ( 999999, Qt::Horizontal, QVariant() ); + QMap roles; + model->sibling ( 0, 0, QModelIndex() ); + model->span ( QModelIndex() ); + model->supportedDropActions(); +} + +/*! + Tests model's implementation of QAbstractItemModel::rowCount() and hasChildren() + + Models that are dynamically populated are not as fully tested here. + */ +void ModelTest::rowCount() +{ +// qDebug() << "rc"; + // check top row + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + int rows = model->rowCount ( topIndex ); + QVERIFY( rows >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( topIndex ) ); + + QModelIndex secondLevelIndex = model->index ( 0, 0, topIndex ); + if ( secondLevelIndex.isValid() ) { // not the top level + // check a row count where parent is valid + rows = model->rowCount ( secondLevelIndex ); + QVERIFY( rows >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( secondLevelIndex ) ); + } + + // The models rowCount() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::columnCount() and hasChildren() + */ +void ModelTest::columnCount() +{ + // check top row + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + QVERIFY( model->columnCount ( topIndex ) >= 0 ); + + // check a column count where parent is valid + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + if ( childIndex.isValid() ) + QVERIFY( model->columnCount ( childIndex ) >= 0 ); + + // columnCount() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::hasIndex() + */ +void ModelTest::hasIndex() +{ +// qDebug() << "hi"; + // Make sure that invalid values returns an invalid index + QVERIFY( !model->hasIndex ( -2, -2 ) ); + QVERIFY( !model->hasIndex ( -2, 0 ) ); + QVERIFY( !model->hasIndex ( 0, -2 ) ); + + int rows = model->rowCount(); + int columns = model->columnCount(); + + // check out of bounds + QVERIFY( !model->hasIndex ( rows, columns ) ); + QVERIFY( !model->hasIndex ( rows + 1, columns + 1 ) ); + + if ( rows > 0 ) + QVERIFY( model->hasIndex ( 0, 0 ) ); + + // hasIndex() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::index() + */ +void ModelTest::index() +{ +// qDebug() << "i"; + // Make sure that invalid values returns an invalid index + QVERIFY( model->index ( -2, -2 ) == QModelIndex() ); + QVERIFY( model->index ( -2, 0 ) == QModelIndex() ); + QVERIFY( model->index ( 0, -2 ) == QModelIndex() ); + + int rows = model->rowCount(); + int columns = model->columnCount(); + + if ( rows == 0 ) + return; + + // Catch off by one errors + QVERIFY( model->index ( rows, columns ) == QModelIndex() ); + QVERIFY( model->index ( 0, 0 ).isValid() ); + + // Make sure that the same index is *always* returned + QModelIndex a = model->index ( 0, 0 ); + QModelIndex b = model->index ( 0, 0 ); + QVERIFY( a == b ); + + // index() is tested more extensively in checkChildren(), + // but this catches the big mistakes +} + +/*! + Tests model's implementation of QAbstractItemModel::parent() + */ +void ModelTest::parent() +{ +// qDebug() << "p"; + // Make sure the model won't crash and will return an invalid QModelIndex + // when asked for the parent of an invalid index. + QVERIFY( model->parent ( QModelIndex() ) == QModelIndex() ); + + if ( model->rowCount() == 0 ) + return; + + // Column 0 | Column 1 | + // QModelIndex() | | + // \- topIndex | topIndex1 | + // \- childIndex | childIndex1 | + + // Common error test #1, make sure that a top level index has a parent + // that is a invalid QModelIndex. + QModelIndex topIndex = model->index ( 0, 0, QModelIndex() ); + QVERIFY( model->parent ( topIndex ) == QModelIndex() ); + + // Common error test #2, make sure that a second level index has a parent + // that is the first level index. + if ( model->rowCount ( topIndex ) > 0 ) { + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + QVERIFY( model->parent ( childIndex ) == topIndex ); + } + + // Common error test #3, the second column should NOT have the same children + // as the first column in a row. + // Usually the second column shouldn't have children. + QModelIndex topIndex1 = model->index ( 0, 1, QModelIndex() ); + if ( model->rowCount ( topIndex1 ) > 0 ) { + QModelIndex childIndex = model->index ( 0, 0, topIndex ); + QModelIndex childIndex1 = model->index ( 0, 0, topIndex1 ); + QVERIFY( childIndex != childIndex1 ); + } + + // Full test, walk n levels deep through the model making sure that all + // parent's children correctly specify their parent. + checkChildren ( QModelIndex() ); +} + +/*! + Called from the parent() test. + + A model that returns an index of parent X should also return X when asking + for the parent of the index. + + This recursive function does pretty extensive testing on the whole model in an + effort to catch edge cases. + + This function assumes that rowCount(), columnCount() and index() already work. + If they have a bug it will point it out, but the above tests should have already + found the basic bugs because it is easier to figure out the problem in + those tests then this one. + */ +void ModelTest::checkChildren ( const QModelIndex &parent, int currentDepth ) +{ + // First just try walking back up the tree. + QModelIndex p = parent; + while ( p.isValid() ) + p = p.parent(); + + // For models that are dynamically populated + if ( model->canFetchMore ( parent ) ) { + fetchingMore = true; + model->fetchMore ( parent ); + fetchingMore = false; + } + + int rows = model->rowCount ( parent ); + int columns = model->columnCount ( parent ); + + if ( rows > 0 ) + QVERIFY( model->hasChildren ( parent ) ); + + // Some further testing against rows(), columns(), and hasChildren() + QVERIFY( rows >= 0 ); + QVERIFY( columns >= 0 ); + if ( rows > 0 ) + QVERIFY( model->hasChildren ( parent ) ); + + //qDebug() << "parent:" << model->data(parent).toString() << "rows:" << rows + // << "columns:" << columns << "parent column:" << parent.column(); + + const QModelIndex topLeftChild = model->index( 0, 0, parent ); + + QVERIFY( !model->hasIndex ( rows + 1, 0, parent ) ); + for ( int r = 0; r < rows; ++r ) { + if ( model->canFetchMore ( parent ) ) { + fetchingMore = true; + model->fetchMore ( parent ); + fetchingMore = false; + } + QVERIFY( !model->hasIndex ( r, columns + 1, parent ) ); + for ( int c = 0; c < columns; ++c ) { + QVERIFY( model->hasIndex ( r, c, parent ) ); + QModelIndex index = model->index ( r, c, parent ); + // rowCount() and columnCount() said that it existed... + QVERIFY( index.isValid() ); + + // index() should always return the same index when called twice in a row + QModelIndex modifiedIndex = model->index ( r, c, parent ); + QVERIFY( index == modifiedIndex ); + + // Make sure we get the same index if we request it twice in a row + QModelIndex a = model->index ( r, c, parent ); + QModelIndex b = model->index ( r, c, parent ); + QVERIFY( a == b ); + + { + const QModelIndex sibling = model->sibling( r, c, topLeftChild ); + QVERIFY( index == sibling ); + } + { + const QModelIndex sibling = topLeftChild.sibling( r, c ); + QVERIFY( index == sibling ); + } + + // Some basic checking on the index that is returned + QVERIFY( index.model() == model ); + QCOMPARE( index.row(), r ); + QCOMPARE( index.column(), c ); + // While you can technically return a QVariant usually this is a sign + // of a bug in data(). Disable if this really is ok in your model. +// QVERIFY( model->data ( index, Qt::DisplayRole ).isValid() ); + + // If the next test fails here is some somewhat useful debug you play with. + + if (model->parent(index) != parent) { + qDebug() << r << c << currentDepth << model->data(index).toString() + << model->data(parent).toString(); + qDebug() << index << parent << model->parent(index); +// And a view that you can even use to show the model. +// QTreeView view; +// view.setModel(model); +// view.show(); + } + + // Check that we can get back our real parent. + QCOMPARE( model->parent ( index ), parent ); + + // recursively go down the children + if ( model->hasChildren ( index ) && currentDepth < 10 ) { + //qDebug() << r << c << "has children" << model->rowCount(index); + checkChildren ( index, ++currentDepth ); + }/* else { if (currentDepth >= 10) qDebug() << "checked 10 deep"; };*/ + + // make sure that after testing the children that the index doesn't change. + QModelIndex newerIndex = model->index ( r, c, parent ); + QVERIFY( index == newerIndex ); + } + } +} + +/*! + Tests model's implementation of QAbstractItemModel::data() + */ +void ModelTest::data() +{ + // Invalid index should return an invalid qvariant + QVERIFY( !model->data ( QModelIndex() ).isValid() ); + + if ( model->rowCount() == 0 ) + return; + + // A valid index should have a valid QVariant data + QVERIFY( model->index ( 0, 0 ).isValid() ); + + // shouldn't be able to set data on an invalid index + QVERIFY( !model->setData ( QModelIndex(), QLatin1String ( "foo" ), Qt::DisplayRole ) ); + + // General Purpose roles that should return a QString + QVariant variant = model->data ( model->index ( 0, 0 ), Qt::ToolTipRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + variant = model->data ( model->index ( 0, 0 ), Qt::StatusTipRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + variant = model->data ( model->index ( 0, 0 ), Qt::WhatsThisRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + + // General Purpose roles that should return a QSize + variant = model->data ( model->index ( 0, 0 ), Qt::SizeHintRole ); + if ( variant.isValid() ) { + QVERIFY( variant.canConvert() ); + } + + // General Purpose roles that should return a QFont + QVariant fontVariant = model->data ( model->index ( 0, 0 ), Qt::FontRole ); + if ( fontVariant.isValid() ) { + QVERIFY( fontVariant.canConvert() ); + } + + // Check that the alignment is one we know about + QVariant textAlignmentVariant = model->data ( model->index ( 0, 0 ), Qt::TextAlignmentRole ); + if ( textAlignmentVariant.isValid() ) { + unsigned int alignment = textAlignmentVariant.toUInt(); + QCOMPARE( alignment, ( alignment & ( Qt::AlignHorizontal_Mask | Qt::AlignVertical_Mask ) ) ); + } + + // General Purpose roles that should return a QColor + QVariant colorVariant = model->data ( model->index ( 0, 0 ), Qt::BackgroundColorRole ); + if ( colorVariant.isValid() ) { + QVERIFY( colorVariant.canConvert() ); + } + + colorVariant = model->data ( model->index ( 0, 0 ), Qt::TextColorRole ); + if ( colorVariant.isValid() ) { + QVERIFY( colorVariant.canConvert() ); + } + + // Check that the "check state" is one we know about. + QVariant checkStateVariant = model->data ( model->index ( 0, 0 ), Qt::CheckStateRole ); + if ( checkStateVariant.isValid() ) { + int state = checkStateVariant.toInt(); + QVERIFY( state == Qt::Unchecked || + state == Qt::PartiallyChecked || + state == Qt::Checked ); + } +} + +/*! + Store what is about to be inserted to make sure it actually happens + + \sa rowsInserted() + */ +void ModelTest::rowsAboutToBeInserted ( const QModelIndex &parent, int start, int /* end */) +{ +// Q_UNUSED(end); +// qDebug() << "rowsAboutToBeInserted" << "start=" << start << "end=" << end << "parent=" << model->data ( parent ).toString() +// << "current count of parent=" << model->rowCount ( parent ); // << "display of last=" << model->data( model->index(start-1, 0, parent) ); +// qDebug() << model->index(start-1, 0, parent) << model->data( model->index(start-1, 0, parent) ); + Changing c; + c.parent = parent; + c.oldSize = model->rowCount ( parent ); + c.last = model->data ( model->index ( start - 1, 0, parent ) ); + c.next = model->data ( model->index ( start, 0, parent ) ); + insert.push ( c ); +} + +/*! + Confirm that what was said was going to happen actually did + + \sa rowsAboutToBeInserted() + */ +void ModelTest::rowsInserted ( const QModelIndex & parent, int start, int end ) +{ + Changing c = insert.pop(); + QVERIFY( c.parent == parent ); +// qDebug() << "rowsInserted" << "start=" << start << "end=" << end << "oldsize=" << c.oldSize +// << "parent=" << model->data ( parent ).toString() << "current rowcount of parent=" << model->rowCount ( parent ); + +// for (int ii=start; ii <= end; ii++) +// { +// qDebug() << "itemWasInserted:" << ii << model->data ( model->index ( ii, 0, parent )); +// } +// qDebug(); + + QVERIFY( c.oldSize + ( end - start + 1 ) == model->rowCount ( parent ) ); + QVERIFY( c.last == model->data ( model->index ( start - 1, 0, c.parent ) ) ); + + if (c.next != model->data(model->index(end + 1, 0, c.parent))) { + qDebug() << start << end; + for (int i=0; i < model->rowCount(); ++i) + qDebug() << model->index(i, 0).data().toString(); + qDebug() << c.next << model->data(model->index(end + 1, 0, c.parent)); + } + + QVERIFY( c.next == model->data ( model->index ( end + 1, 0, c.parent ) ) ); +} + +void ModelTest::layoutAboutToBeChanged() +{ + for ( int i = 0; i < qBound ( 0, model->rowCount(), 100 ); ++i ) + changing.append ( QPersistentModelIndex ( model->index ( i, 0 ) ) ); +} + +void ModelTest::layoutChanged() +{ + for ( int i = 0; i < changing.count(); ++i ) { + QPersistentModelIndex p = changing[i]; + QVERIFY( p == model->index ( p.row(), p.column(), p.parent() ) ); + } + changing.clear(); +} + +/*! + Store what is about to be inserted to make sure it actually happens + + \sa rowsRemoved() + */ +void ModelTest::rowsAboutToBeRemoved ( const QModelIndex &parent, int start, int end ) +{ +qDebug() << "ratbr" << parent << start << end; + Changing c; + c.parent = parent; + c.oldSize = model->rowCount ( parent ); + c.last = model->data ( model->index ( start - 1, 0, parent ) ); + c.next = model->data ( model->index ( end + 1, 0, parent ) ); + remove.push ( c ); +} + +/*! + Confirm that what was said was going to happen actually did + + \sa rowsAboutToBeRemoved() + */ +void ModelTest::rowsRemoved ( const QModelIndex & parent, int start, int end ) +{ + qDebug() << "rr" << parent << start << end; + Changing c = remove.pop(); + QVERIFY( c.parent == parent ); + QVERIFY( c.oldSize - ( end - start + 1 ) == model->rowCount ( parent ) ); + QVERIFY( c.last == model->data ( model->index ( start - 1, 0, c.parent ) ) ); + QVERIFY( c.next == model->data ( model->index ( start, 0, c.parent ) ) ); +} + +void ModelTest::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight) +{ + QVERIFY(topLeft.isValid()); + QVERIFY(bottomRight.isValid()); + QModelIndex commonParent = bottomRight.parent(); + QVERIFY(topLeft.parent() == commonParent); + QVERIFY(topLeft.row() <= bottomRight.row()); + QVERIFY(topLeft.column() <= bottomRight.column()); + int rowCount = model->rowCount(commonParent); + int columnCount = model->columnCount(commonParent); + QVERIFY(bottomRight.row() < rowCount); + QVERIFY(bottomRight.column() < columnCount); +} + +void ModelTest::headerDataChanged(Qt::Orientation orientation, int start, int end) +{ + QVERIFY(start >= 0); + QVERIFY(end >= 0); + QVERIFY(start <= end); + int itemCount = orientation == Qt::Vertical ? model->rowCount() : model->columnCount(); + QVERIFY(start < itemCount); + QVERIFY(end < itemCount); +} + diff --git a/kcmkwin/kwincompositing/test/modeltest.h b/kcmkwin/kwincompositing/test/modeltest.h new file mode 100644 index 0000000..49d25d9 --- /dev/null +++ b/kcmkwin/kwincompositing/test/modeltest.h @@ -0,0 +1,96 @@ +/**************************************************************************** +** +** Copyright (C) 2013 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** This file is part of the test suite of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + + +#ifndef MODELTEST_H +#define MODELTEST_H + +#include +#include +#include + +class ModelTest : public QObject +{ + Q_OBJECT + +public: + ModelTest( QAbstractItemModel *model, QObject *parent = 0 ); + +private Q_SLOTS: + void nonDestructiveBasicTest(); + void rowCount(); + void columnCount(); + void hasIndex(); + void index(); + void parent(); + void data(); + +protected Q_SLOTS: + void runAllTests(); + void layoutAboutToBeChanged(); + void layoutChanged(); + void rowsAboutToBeInserted( const QModelIndex &parent, int start, int end ); + void rowsInserted( const QModelIndex & parent, int start, int end ); + void rowsAboutToBeRemoved( const QModelIndex &parent, int start, int end ); + void rowsRemoved( const QModelIndex & parent, int start, int end ); + void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight); + void headerDataChanged(Qt::Orientation orientation, int start, int end); + +private: + void checkChildren( const QModelIndex &parent, int currentDepth = 0 ); + + QAbstractItemModel *model; + + struct Changing { + QModelIndex parent; + int oldSize; + QVariant last; + QVariant next; + }; + QStack insert; + QStack remove; + + bool fetchingMore; + + QList changing; +}; + +#endif diff --git a/kcmkwin/kwindecoration/CMakeLists.txt b/kcmkwin/kwindecoration/CMakeLists.txt new file mode 100644 index 0000000..e04ed3a --- /dev/null +++ b/kcmkwin/kwindecoration/CMakeLists.txt @@ -0,0 +1,41 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcmkwindecoration\") + +add_subdirectory(declarative-plugin) + +set(kcm_kwindecoration_PART_SRCS + kcm.cpp + decorationmodel.cpp + declarative-plugin/buttonsmodel.cpp +) + +ki18n_wrap_ui(kcm_kwindecoration_PART_SRCS + kcm.ui +) + +add_library(kcm_kwindecoration MODULE ${kcm_kwindecoration_PART_SRCS}) +target_link_libraries(kcm_kwindecoration + KDecoration2::KDecoration + Qt5::DBus + Qt5::Quick + Qt5::QuickWidgets + Qt5::UiTools + KF5::Completion + KF5::ConfigWidgets + KF5::Declarative + KF5::I18n + KF5::NewStuff + KF5::WindowSystem + KF5::Service +) +install(TARGETS kcm_kwindecoration DESTINATION ${PLUGIN_INSTALL_DIR} ) + +########### install files ############### + +install( FILES kwindecoration.desktop DESTINATION ${SERVICES_INSTALL_DIR} ) +install( FILES + qml/main.qml + qml/Buttons.qml + qml/ButtonGroup.qml + qml/Previews.qml + DESTINATION ${DATA_INSTALL_DIR}/kwin/kcm_kwindecoration) diff --git a/kcmkwin/kwindecoration/Messages.sh b/kcmkwin/kwindecoration/Messages.sh new file mode 100644 index 0000000..3f9d8a3 --- /dev/null +++ b/kcmkwin/kwindecoration/Messages.sh @@ -0,0 +1,4 @@ +#!bin/sh +$EXTRACTRC `find . -name \*.rc -o -name \*.ui -o -name \*.kcfg` >> rc.cpp +$XGETTEXT `find . -name \*.qml -o -name \*.cpp -o -name \*.h` -o $podir/kcmkwindecoration.pot +rm -f rc.cpp 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..b417525 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/buttonsmodel.cpp @@ -0,0 +1,183 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#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 + 1, index+1); + m_buttons.insert(index+1, 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: + http://doc.qt.nokia.com/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(); +} + +} +} + diff --git a/kcmkwin/kwindecoration/declarative-plugin/buttonsmodel.h b/kcmkwin/kwindecoration/declarative-plugin/buttonsmodel.h new file mode 100644 index 0000000..13f628d --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/buttonsmodel.h @@ -0,0 +1,64 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#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 = 0); + explicit ButtonsModel(QObject *parent = nullptr); + virtual ~ButtonsModel(); + 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 remove(int index); + Q_INVOKABLE void up(int index); + Q_INVOKABLE void down(int index); + Q_INVOKABLE void move(int sourceIndex, int targetIndex); + + 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..1eb823b --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/plugin.cpp @@ -0,0 +1,55 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "plugin.h" +#include "buttonsmodel.h" +#include "previewbutton.h" +#include "previewbridge.h" +#include "previewclient.h" +#include "previewitem.h" +#include "previewsettings.h" + +#include +#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"); + qmlRegisterType(); + qmlRegisterType(); + qmlRegisterType(); + qmlRegisterType(); +} + +} +} + + diff --git a/kcmkwin/kwindecoration/declarative-plugin/plugin.h b/kcmkwin/kwindecoration/declarative-plugin/plugin.h new file mode 100644 index 0000000..574bdb2 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/plugin.h @@ -0,0 +1,41 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#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..7a74077 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewbridge.cpp @@ -0,0 +1,255 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#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 + +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 std::move(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 std::move(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; + qDebug() << "Plugin changed to: " << m_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); + qDebug() <<"Plugin not set"; + return; + } + const auto offers = KPluginTrader::self()->query(s_pluginName, + s_pluginName, + QStringLiteral("[X-KDE-PluginInfo-Name] == '%1'").arg(m_plugin)); + if (offers.isEmpty()) { + setValid(false); + qDebug() << "no offers"; + return; + } + KPluginLoader loader(offers.first().libraryPath()); + m_factory = loader.factory(); + qDebug() << "Factory: " << !m_factory.isNull(); + 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() +{ + if (!m_valid) { + return; + } + //setup the UI + QDialog dialog; + 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::Apply | + QDialogButtonBox::RestoreDefaults | + QDialogButtonBox::Reset, + &dialog); + + QPushButton *apply = buttons->button(QDialogButtonBox::Apply); + QPushButton *reset = buttons->button(QDialogButtonBox::Reset); + apply->setEnabled(false); + 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(apply, &QPushButton::clicked, this, save); + connect(reset, &QPushButton::clicked, kcm, &KCModule::load); + auto changedSignal = static_cast(&KCModule::changed); + connect(kcm, changedSignal, apply, &QPushButton::setEnabled); + 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); + dialog.exec(); +} + +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..fe366eb --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewbridge.h @@ -0,0 +1,139 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef KDECOARTIONS_PREVIEW_BRIDGE_H +#define KDECOARTIONS_PREVIEW_BRIDGE_H + +#include +#include + +#include +#include + +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); + virtual ~PreviewBridge(); + 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(); + +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); + virtual ~BridgeItem(); + + 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..2a5d342 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewbutton.cpp @@ -0,0 +1,139 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#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; + } + m_button->paint(painter, QRect(0, 0, width(), height())); +} + +} +} diff --git a/kcmkwin/kwindecoration/declarative-plugin/previewbutton.h b/kcmkwin/kwindecoration/declarative-plugin/previewbutton.h new file mode 100644 index 0000000..0afdf30 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewbutton.h @@ -0,0 +1,81 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef KDECOARTIONS_PREVIEW_BUTTON_ITEM_H +#define KDECOARTIONS_PREVIEW_BUTTON_ITEM_H + +#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) + +public: + explicit PreviewButtonItem(QQuickItem *parent = nullptr); + virtual ~PreviewButtonItem(); + 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); + +Q_SIGNALS: + void bridgeChanged(); + void typeChanged(); + void settingsChanged(); + +protected: + void componentComplete() override; + +private: + void createButton(); + void syncGeometry(); + 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..c5856eb --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewclient.cpp @@ -0,0 +1,465 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#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::modalChanged, c, &DecoratedClient::modalChanged); + 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::, c, &DecoratedClient::); + 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); + + 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; +} + +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) +{ + qDebug() << "tooltip show requested with text:" << text; +} + +void PreviewClient::requestHideToolTip() +{ + qDebug() << "tooltip hide requested"; +} + +void PreviewClient::requestClose() +{ + emit closeRequested(); +} + +void PreviewClient::requestContextHelp() +{ + qDebug() << "context help requested"; +} + +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; \ + } \ + qDebug() << "Setting " << #variable << ":" << variable;\ + 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..df36083 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewclient.h @@ -0,0 +1,214 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#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); + virtual ~PreviewClient(); + + 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; + 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); + + 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..534f6e1 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewitem.cpp @@ -0,0 +1,500 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#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().background().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; + } + m_decoration = m_bridge->createDecoration(0); + if (!m_decoration) { + return; + } + m_decoration->setProperty("visualParent", QVariant::fromValue(this)); + m_client = m_bridge->lastCreatedClient(); + connect(m_decoration, &Decoration::bordersChanged, this, &PreviewItem::syncSize); + connect(m_decoration, &Decoration::shadowChanged, this, &PreviewItem::syncSize); + emit decorationChanged(m_decoration); +} + +Decoration *PreviewItem::decoration() const +{ + return m_decoration; +} + +void PreviewItem::setDecoration(Decoration *deco) +{ + if (m_decoration == deco) { + return; + } + auto updateSlot = static_cast(&QQuickItem::update); + if (m_decoration) { + disconnect(m_decoration, &Decoration::bordersChanged, this, updateSlot); + } + m_decoration = deco; + m_decoration->setProperty("visualParent", QVariant::fromValue(this)); + connect(m_decoration, &Decoration::bordersChanged, this, updateSlot); + connect(m_decoration, &Decoration::sectionUnderMouseChanged, this, + [this](Qt::WindowFrameSection section) { + switch (section) { + case Qt::TopRightSection: + case Qt::BottomLeftSection: + setCursor(Qt::SizeBDiagCursor); + return; + case Qt::TopLeftSection: + case Qt::BottomRightSection: + setCursor(Qt::SizeFDiagCursor); + return; + case Qt::TopSection: + case Qt::BottomSection: + setCursor(Qt::SizeVerCursor); + return; + case Qt::LeftSection: + case Qt::RightSection: + setCursor(Qt::SizeHorCursor); + return; + default: + setCursor(Qt::ArrowCursor); + } + } + ); + connect(m_decoration, &KDecoration2::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..e052751 --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewitem.h @@ -0,0 +1,104 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#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); + virtual ~PreviewItem(); + void paint(QPainter *painter); + + 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..dd996da --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewsettings.cpp @@ -0,0 +1,279 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#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..dcebdab --- /dev/null +++ b/kcmkwin/kwindecoration/declarative-plugin/previewsettings.h @@ -0,0 +1,163 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#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 = 0); + virtual ~BorderSizesModel(); + 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); + virtual ~PreviewSettings(); + 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 { + 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); + virtual ~Settings(); + + 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..2bd1735 --- /dev/null +++ b/kcmkwin/kwindecoration/decorationmodel.cpp @@ -0,0 +1,196 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "decorationmodel.h" +// KDecoration2 +#include +#include +// KDE +#include +#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 Qt::UserRole +4: + return d.pluginName; + case Qt::UserRole +5: + return d.themeName; + case Qt::UserRole +6: + return d.configuration; + } + + return QVariant(); +} + +QHash< int, QByteArray > DecorationsModel::roleNames() const +{ + QHash roles({ + {Qt::DisplayRole, QByteArrayLiteral("display")}, + {Qt::UserRole + 4, QByteArrayLiteral("plugin")}, + {Qt::UserRole + 5, QByteArrayLiteral("theme")}, + {Qt::UserRole +6, QByteArrayLiteral("configureable")} + }); + 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 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); + bool config = false; + if (!metadata.isUndefined()) { + const auto decoSettingsMap = metadata.toObject().toVariantMap(); + const QString &kns = findKNewStuff(decoSettingsMap); + if (!kns.isEmpty()) { + m_knsProvides.insert(kns, info.name().isEmpty() ? info.pluginName() : info.name()); + } + 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; + } + config = isConfigureable(decoSettingsMap); + } + Data data; + data.pluginName = info.pluginName(); + data.visibleName = info.name().isEmpty() ? info.pluginName() : info.name(); + data.configuration = config; + + 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..41af88d --- /dev/null +++ b/kcmkwin/kwindecoration/decorationmodel.h @@ -0,0 +1,65 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef KDECORATION_DECORATION_MODEL_H +#define KDECORATION_DECORATION_MODEL_H + +#include + +namespace KDecoration2 +{ + +namespace Configuration +{ + +class DecorationsModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit DecorationsModel(QObject *parent = nullptr); + virtual ~DecorationsModel(); + + 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; + + QMap knsProviders() const { + return m_knsProvides; + } + +public Q_SLOTS: + void init(); + +private: + struct Data { + QString pluginName; + QString themeName; + QString visibleName; + bool configuration = false; + }; + std::vector m_plugins; + QMap m_knsProvides; +}; + +} +} + +#endif diff --git a/kcmkwin/kwindecoration/kcm.cpp b/kcmkwin/kwindecoration/kcm.cpp new file mode 100644 index 0000000..995041c --- /dev/null +++ b/kcmkwin/kwindecoration/kcm.cpp @@ -0,0 +1,435 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#include "kcm.h" +#include "decorationmodel.h" +#include "declarative-plugin/buttonsmodel.h" +#include + +// KDE +#include +#include +#include +#include +#include +#include +// Qt +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +K_PLUGIN_FACTORY(KDecorationFactory, + registerPlugin(); + ) + +Q_DECLARE_METATYPE(KDecoration2::BorderSize) + +namespace KDecoration2 +{ + +namespace Configuration +{ +static const QString s_pluginName = QStringLiteral("org.kde.kdecoration2"); +#if HAVE_BREEZE_DECO +static const QString s_defaultPlugin = QStringLiteral(BREEZE_KDECORATION_PLUGIN_ID); +static const QString s_defaultTheme; +#else +static const QString s_defaultPlugin = QStringLiteral("org.kde.kwin.aurorae"); +static const QString s_defaultTheme = QStringLiteral("kwin4_decoration_qml_plastik"); +#endif +static const QString s_borderSizeNormal = QStringLiteral("Normal"); +static const QString s_ghnsIcon = QStringLiteral("get-hot-new-stuff"); + +ConfigurationForm::ConfigurationForm(QWidget *parent) + : QWidget(parent) +{ + setupUi(this); +} + +static bool s_loading = false; + +ConfigurationModule::ConfigurationModule(QWidget *parent, const QVariantList &args) + : KCModule(parent, args) + , m_model(new DecorationsModel(this)) + , m_proxyModel(new QSortFilterProxyModel(this)) + , m_ui(new ConfigurationForm(this)) + , m_leftButtons(new Preview::ButtonsModel(QVector(), this)) + , m_rightButtons(new Preview::ButtonsModel(QVector(), this)) + , m_availableButtons(new Preview::ButtonsModel(this)) +{ + m_proxyModel->setSourceModel(m_model); + m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); + m_proxyModel->setSortCaseSensitivity(Qt::CaseInsensitive); + m_proxyModel->sort(0); + connect(m_ui->filter, &QLineEdit::textChanged, m_proxyModel, &QSortFilterProxyModel::setFilterFixedString); + + m_quickView = new QQuickView(0); + KDeclarative::KDeclarative kdeclarative; + kdeclarative.setDeclarativeEngine(m_quickView->engine()); + kdeclarative.setTranslationDomain(QStringLiteral(TRANSLATION_DOMAIN)); + kdeclarative.setupContext(); + kdeclarative.setupEngine(m_quickView->engine()); + + qmlRegisterType(); + QWidget *widget = QWidget::createWindowContainer(m_quickView, this); + QVBoxLayout* layout = new QVBoxLayout(m_ui->view); + layout->setContentsMargins(0,0,0,0); + layout->addWidget(widget); + + m_quickView->rootContext()->setContextProperty(QStringLiteral("decorationsModel"), m_proxyModel); + updateColors(); + m_quickView->rootContext()->setContextProperty("_borderSizesIndex", 3); // 3 is normal + m_quickView->rootContext()->setContextProperty("leftButtons", m_leftButtons); + m_quickView->rootContext()->setContextProperty("rightButtons", m_rightButtons); + m_quickView->rootContext()->setContextProperty("availableButtons", m_availableButtons); + + m_quickView->rootContext()->setContextProperty("titleFont", QFontDatabase::systemFont(QFontDatabase::TitleFont)); + m_quickView->setResizeMode(QQuickView::SizeRootObjectToView); + m_quickView->setSource(QUrl::fromLocalFile(QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kwin/kcm_kwindecoration/main.qml")))); + if (m_quickView->status() == QQuickView::Ready) { + auto listView = m_quickView->rootObject()->findChild("listView"); + if (listView) { + connect(listView, SIGNAL(currentIndexChanged()), this, SLOT(changed())); + } + } + + m_ui->tabWidget->tabBar()->disconnect(); + auto setCurrentTab = [this](int index) { + if (index == 0) + m_ui->doubleClickMessage->hide(); + m_ui->filter->setVisible(index == 0); + m_ui->knsButton->setVisible(index == 0); + if (auto themeList = m_quickView->rootObject()->findChild("themeList")) { + themeList->setVisible(index == 0); + } + m_ui->borderSizesLabel->setVisible(index == 0); + m_ui->borderSizesCombo->setVisible(index == 0); + + m_ui->closeWindowsDoubleClick->setVisible(index == 1); + if (auto buttonLayout = m_quickView->rootObject()->findChild("buttonLayout")) { + buttonLayout->setVisible(index == 1); + } + }; + connect(m_ui->tabWidget->tabBar(), &QTabBar::currentChanged, this, setCurrentTab); + setCurrentTab(0); + + m_ui->doubleClickMessage->setVisible(false); + m_ui->doubleClickMessage->setText(i18n("Close by double clicking:\n To open the menu, keep the button pressed until it appears.")); + m_ui->doubleClickMessage->setCloseButtonVisible(true); + m_ui->borderSizesCombo->setItemData(0, QVariant::fromValue(BorderSize::None)); + m_ui->borderSizesCombo->setItemData(1, QVariant::fromValue(BorderSize::NoSides)); + m_ui->borderSizesCombo->setItemData(2, QVariant::fromValue(BorderSize::Tiny)); + m_ui->borderSizesCombo->setItemData(3, QVariant::fromValue(BorderSize::Normal)); + m_ui->borderSizesCombo->setItemData(4, QVariant::fromValue(BorderSize::Large)); + m_ui->borderSizesCombo->setItemData(5, QVariant::fromValue(BorderSize::VeryLarge)); + m_ui->borderSizesCombo->setItemData(6, QVariant::fromValue(BorderSize::Huge)); + m_ui->borderSizesCombo->setItemData(7, QVariant::fromValue(BorderSize::VeryHuge)); + m_ui->borderSizesCombo->setItemData(8, QVariant::fromValue(BorderSize::Oversized)); + m_ui->knsButton->setIcon(QIcon::fromTheme(s_ghnsIcon)); + + auto changedSlot = static_cast(&ConfigurationModule::changed); + connect(m_ui->closeWindowsDoubleClick, &QCheckBox::stateChanged, this, changedSlot); + connect(m_ui->closeWindowsDoubleClick, &QCheckBox::toggled, this, + [this] (bool toggled) { + if (s_loading) { + return; + } + if (toggled) + m_ui->doubleClickMessage->animatedShow(); + else + m_ui->doubleClickMessage->animatedHide(); + } + ); + connect(m_ui->borderSizesCombo, static_cast(&QComboBox::currentIndexChanged), + this, [this] (int index) { + auto listView = m_quickView->rootObject()->findChild("listView"); + if (listView) { + listView->setProperty("borderSizesIndex", index); + } + changed(); + } + ); + connect(m_model, &QAbstractItemModel::modelReset, this, + [this] { + const auto &kns = m_model->knsProviders(); + m_ui->knsButton->setEnabled(!kns.isEmpty()); + if (kns.isEmpty()) { + return; + } + if (kns.count() > 1) { + QMenu *menu = new QMenu(m_ui->knsButton); + for (auto it = kns.begin(); it != kns.end(); ++it) { + QAction *action = menu->addAction(QIcon::fromTheme(s_ghnsIcon), it.value()); + action->setData(it.key()); + connect(action, &QAction::triggered, this, [this, action] { showKNS(action->data().toString());}); + } + m_ui->knsButton->setMenu(menu); + } + } + ); + connect(m_ui->knsButton, &QPushButton::clicked, this, + [this] { + const auto &kns = m_model->knsProviders(); + if (kns.isEmpty()) { + return; + } + showKNS(kns.firstKey()); + } + ); + connect(m_leftButtons, &QAbstractItemModel::rowsInserted, this, changedSlot); + connect(m_leftButtons, &QAbstractItemModel::rowsMoved, this, changedSlot); + connect(m_leftButtons, &QAbstractItemModel::rowsRemoved, this, changedSlot); + connect(m_rightButtons, &QAbstractItemModel::rowsInserted, this, changedSlot); + connect(m_rightButtons, &QAbstractItemModel::rowsMoved, this, changedSlot); + connect(m_rightButtons, &QAbstractItemModel::rowsRemoved, this, changedSlot); + + QVBoxLayout *l = new QVBoxLayout(this); + l->addWidget(m_ui); + QMetaObject::invokeMethod(m_model, "init", Qt::QueuedConnection); + + m_ui->installEventFilter(this); +} + +ConfigurationModule::~ConfigurationModule() = default; + +void ConfigurationModule::showEvent(QShowEvent *ev) +{ + KCModule::showEvent(ev); +} + +static const QMap s_sizes = QMap({ + {QStringLiteral("None"), BorderSize::None}, + {QStringLiteral("NoSides"), BorderSize::NoSides}, + {QStringLiteral("Tiny"), BorderSize::Tiny}, + {s_borderSizeNormal, BorderSize::Normal}, + {QStringLiteral("Large"), BorderSize::Large}, + {QStringLiteral("VeryLarge"), BorderSize::VeryLarge}, + {QStringLiteral("Huge"), BorderSize::Huge}, + {QStringLiteral("VeryHuge"), BorderSize::VeryHuge}, + {QStringLiteral("Oversized"), BorderSize::Oversized} +}); + +static BorderSize stringToSize(const QString &name) +{ + auto it = s_sizes.constFind(name); + if (it == s_sizes.constEnd()) { + // non sense values are interpreted just like normal + return BorderSize::Normal; + } + return it.value(); +} + +static QString sizeToString(BorderSize size) +{ + return s_sizes.key(size, s_borderSizeNormal); +} + +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; +} + +static +QVector< KDecoration2::DecorationButtonType > readDecorationButtons(const KConfigGroup &config, + const char *key, + const QVector< KDecoration2::DecorationButtonType > &defaultValue) +{ + 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))); +} + +void ConfigurationModule::load() +{ + s_loading = true; + const KConfigGroup config = KSharedConfig::openConfig("kwinrc")->group(s_pluginName); + const QString plugin = config.readEntry("library", s_defaultPlugin); + const QString theme = config.readEntry("theme", s_defaultTheme); + m_ui->closeWindowsDoubleClick->setChecked(config.readEntry("CloseOnDoubleClickOnMenu", false)); + const QVariant border = QVariant::fromValue(stringToSize(config.readEntry("BorderSize", s_borderSizeNormal))); + m_ui->borderSizesCombo->setCurrentIndex(m_ui->borderSizesCombo->findData(border)); + + int themeIndex = m_proxyModel->mapFromSource(m_model->findDecoration(plugin, theme)).row(); + m_quickView->rootContext()->setContextProperty("savedIndex", themeIndex); + + // buttons + const auto &left = readDecorationButtons(config, "ButtonsOnLeft", QVector{ + KDecoration2::DecorationButtonType::Menu, + KDecoration2::DecorationButtonType::OnAllDesktops + }); + while (m_leftButtons->rowCount() > 0) { + m_leftButtons->remove(0); + } + for (auto it = left.begin(); it != left.end(); ++it) { + m_leftButtons->add(*it); + } + const auto &right = readDecorationButtons(config, "ButtonsOnRight", QVector{ + KDecoration2::DecorationButtonType::ContextHelp, + KDecoration2::DecorationButtonType::Minimize, + KDecoration2::DecorationButtonType::Maximize, + KDecoration2::DecorationButtonType::Close + }); + while (m_rightButtons->rowCount() > 0) { + m_rightButtons->remove(0); + } + for (auto it = right.begin(); it != right.end(); ++it) { + m_rightButtons->add(*it); + } + + KCModule::load(); + s_loading = false; +} + +void ConfigurationModule::save() +{ + KConfigGroup config = KSharedConfig::openConfig("kwinrc")->group(s_pluginName); + config.writeEntry("CloseOnDoubleClickOnMenu", m_ui->closeWindowsDoubleClick->isChecked()); + config.writeEntry("BorderSize", sizeToString(m_ui->borderSizesCombo->currentData().value())); + if (auto listView = m_quickView->rootObject()->findChild("listView")) { + const int currentIndex = listView->property("currentIndex").toInt(); + if (currentIndex != -1) { + const QModelIndex index = m_proxyModel->index(currentIndex, 0); + if (index.isValid()) { + config.writeEntry("library", index.data(Qt::UserRole + 4).toString()); + const QString theme = index.data(Qt::UserRole +5).toString(); + if (theme.isEmpty()) { + config.deleteEntry("theme"); + } else { + config.writeEntry("theme", theme); + } + } + } + } + config.writeEntry("ButtonsOnLeft", buttonsToString(m_leftButtons->buttons())); + config.writeEntry("ButtonsOnRight", buttonsToString(m_rightButtons->buttons())); + config.sync(); + KCModule::save(); + // Send signal to all kwin instances + QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KWin"), + QStringLiteral("org.kde.KWin"), + QStringLiteral("reloadConfig")); + QDBusConnection::sessionBus().send(message); +} + +void ConfigurationModule::defaults() +{ + if (auto listView = m_quickView->rootObject()->findChild("listView")) { + const QModelIndex index = m_proxyModel->mapFromSource(m_model->findDecoration(s_defaultPlugin)); + listView->setProperty("currentIndex", index.isValid() ? index.row() : -1); + } + m_ui->borderSizesCombo->setCurrentIndex(m_ui->borderSizesCombo->findData(QVariant::fromValue(stringToSize(s_borderSizeNormal)))); + m_ui->closeWindowsDoubleClick->setChecked(false); + KCModule::defaults(); +} + +void ConfigurationModule::showKNS(const QString &config) +{ + QPointer downloadDialog = new KNS3::DownloadDialog(config, this); + if (downloadDialog->exec() == QDialog::Accepted && !downloadDialog->changedEntries().isEmpty()) { + auto listView = m_quickView->rootObject()->findChild("listView"); + QString selectedPluginName; + QString selectedThemeName; + if (listView) { + const QModelIndex index = m_proxyModel->index(listView->property("currentIndex").toInt(), 0); + if (index.isValid()) { + selectedPluginName = index.data(Qt::UserRole + 4).toString(); + selectedThemeName = index.data(Qt::UserRole + 5).toString(); + } + } + m_model->init(); + if (!selectedPluginName.isEmpty()) { + const QModelIndex index = m_model->findDecoration(selectedPluginName, selectedThemeName); + const QModelIndex proxyIndex = m_proxyModel->mapFromSource(index); + if (listView) { + listView->setProperty("currentIndex", proxyIndex.isValid() ? proxyIndex.row() : -1); + } + } + } + delete downloadDialog; +} + +bool ConfigurationModule::eventFilter(QObject *watched, QEvent *e) +{ + if (watched != m_ui) { + return false; + } + if (e->type() == QEvent::PaletteChange) { + updateColors(); + } + return false; +} + +void ConfigurationModule::updateColors() +{ + m_quickView->rootContext()->setContextProperty("backgroundColor", m_ui->palette().color(QPalette::Active, QPalette::Window)); + m_quickView->rootContext()->setContextProperty("highlightColor", m_ui->palette().color(QPalette::Active, QPalette::Highlight)); + m_quickView->rootContext()->setContextProperty("baseColor", m_ui->palette().color(QPalette::Active, QPalette::Base)); +} + +} +} + +#include "kcm.moc" diff --git a/kcmkwin/kwindecoration/kcm.h b/kcmkwin/kwindecoration/kcm.h new file mode 100644 index 0000000..99fa431 --- /dev/null +++ b/kcmkwin/kwindecoration/kcm.h @@ -0,0 +1,80 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +#ifndef KDECORATIONS_KCM_H +#define KDECORATIONS_KCM_H + +#include +#include +#include + +class QSortFilterProxyModel; +class QQuickView; + +namespace KDecoration2 +{ +namespace Preview +{ +class PreviewBridge; +class ButtonsModel; +} +namespace Configuration +{ +class DecorationsModel; + +class ConfigurationForm : public QWidget, public Ui::KCMForm +{ +public: + explicit ConfigurationForm(QWidget* parent); +}; + +class ConfigurationModule : public KCModule +{ + Q_OBJECT +public: + explicit ConfigurationModule(QWidget *parent = nullptr, const QVariantList &args = QVariantList()); + virtual ~ConfigurationModule(); + + bool eventFilter(QObject *watched, QEvent *e) override; + +public Q_SLOTS: + void defaults() override; + void load() override; + void save() override; + +protected: + void showEvent(QShowEvent *ev) override; + +private: + void showKNS(const QString &config); + void updateColors(); + DecorationsModel *m_model; + QSortFilterProxyModel *m_proxyModel; + ConfigurationForm *m_ui; + QQuickView *m_quickView; + Preview::ButtonsModel *m_leftButtons; + Preview::ButtonsModel *m_rightButtons; + Preview::ButtonsModel *m_availableButtons; +}; + +} + +} + +#endif diff --git a/kcmkwin/kwindecoration/kcm.ui b/kcmkwin/kwindecoration/kcm.ui new file mode 100644 index 0000000..ff29831 --- /dev/null +++ b/kcmkwin/kwindecoration/kcm.ui @@ -0,0 +1,182 @@ + + + KCMForm + + + + 0 + 0 + 386 + 272 + + + + + + + 0 + + + + Theme + + + + + + + + Search + + + true + + + + + + + false + + + Get New Decorations... + + + + + + + + + + + + Close windows by double clicking &the menu button + + + + + + + + + + + 0 + 0 + + + + + 0 + 100 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Border si&ze: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + borderSizesCombo + + + + + + + + No Borders + + + + + No Side Borders + + + + + Tiny + + + + + Normal + + + + + Large + + + + + Very Large + + + + + Huge + + + + + Very Huge + + + + + Oversized + + + + + + + + + + + Buttons + + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+ + KMessageWidget + QFrame +
kmessagewidget.h
+ 1 +
+
+ + +
diff --git a/kcmkwin/kwindecoration/kwindecoration.desktop b/kcmkwin/kwindecoration/kwindecoration.desktop new file mode 100644 index 0000000..9f7230e --- /dev/null +++ b/kcmkwin/kwindecoration/kwindecoration.desktop @@ -0,0 +1,165 @@ +[Desktop Entry] +Exec=kcmshell5 kwindecoration +Icon=preferences-system-windows-action +Type=Service +X-KDE-ServiceTypes=KCModule +X-DocPath=kcontrol/kwindecoration/index.html + +X-KDE-Library=kcm_kwindecoration +X-KDE-ParentApp=kcontrol + +X-KDE-System-Settings-Parent-Category=applicationstyle +X-KDE-Weight=40 + +Name=Window Decorations +Name[ar]=زخارف النوافذ +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]=Decoración de 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]=Window Decorations +Name[is]=Gluggaskreytingar +Name[it]=Decorazioni delle finestre +Name[ja]=ウィンドウの飾り +Name[kk]=Терезенің безендірулері +Name[km]=ការ​តុបតែង​បង្អួច +Name[kn]=ವಿಂಡೋ ಅಲಂಕಾರಗಳು +Name[ko]=창 장식 +Name[lt]=Lango 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=Look and Feel of Window Titles +Comment[bs]=Izgled i osjećaj naslova prozora +Comment[ca]=Aspecte i comportament dels títols de les finestres +Comment[ca@valencia]=Aspecte i comportament dels títols de les finestres +Comment[cs]=Vzhled a dekorace titulků oken +Comment[da]=Udseendet af vinduestitler +Comment[de]=Erscheinungsbild von Fenstertiteln +Comment[el]=Διαμόρφωση της εμφάνισης και αίσθησης του τίτλου των παραθύρων +Comment[en_GB]=Look and Feel of Window Titles +Comment[es]=Aspecto visual de los títulos de las ventanas +Comment[et]=Akna tiitliribade välimus ja tunnetus +Comment[eu]=Leiho-tituluen itxura eta izaera +Comment[fi]=Ikkunoiden kehysten ulkoasu +Comment[fr]=Apparence des titres de fenêtre +Comment[gl]=Aparencia e o comportamento dos títulos das xanelas +Comment[he]=הגדרת המראה והתחושה של מסגרות החלונות +Comment[hu]=Az ablakok címsorának megjelenése +Comment[ia]=Semblantia de titulos de fenestra +Comment[id]=Look and Feel pada Judul Jendela +Comment[it]=Aspetto dei titoli delle finestre +Comment[ja]=ウィンドウタイトルの外観を設定 +Comment[ko]=창 제목 표시줄의 모습과 느낌 설정 +Comment[lt]=Langų antraščių išvaizda ir elgsena +Comment[nb]=Utseende og virkemåte for vindustitlene +Comment[nds]=Dat Utsehn vun de Finstertiteln instellen +Comment[nl]=Uiterlijk en gedrag van venstertitels +Comment[nn]=Utsjånad og åtferd for vindaugstitlar +Comment[pa]=ਵਿੰਡੋ ਟਾਇਟਲਾਂ ਦੇ ਰੰਗ-ਰੂਪ ਦੀ ਸੰਰਚਨਾ +Comment[pl]=Tytuły okien - wrażenia wzrokowe i dotykowe +Comment[pt]=Aparência e Comportamento dos Títulos das Janelas +Comment[pt_BR]=Aparência do título das janelas +Comment[ru]=Настройка внешнего вида заголовков окон +Comment[sk]=Nastavenie vzhľadu titulkov okien +Comment[sl]=Videz in občutek naslovnih vrstic okna +Comment[sr]=Изглед и осећај за наслове прозора +Comment[sr@ijekavian]=Изглед и осјећај за наслове прозора +Comment[sr@ijekavianlatin]=Izgled i osjećaj za naslove prozora +Comment[sr@latin]=Izgled i osećaj za naslove prozora +Comment[sv]=Namnlisternas utseende och känsla +Comment[tr]=Pencere Başlıklarının Görünüm ve Davranışlarını Yapılandır +Comment[uk]=Вигляд і поведінка смужок заголовків вікон +Comment[x-test]=xxLook and Feel of Window Titlesxx +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[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,jendela,pengelola,batas,gaya,tema,tampilan,rasa,tata letak,tombol,pegangan,tepi,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[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[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 + +Categories=Qt;KDE;X-KDE-settings-looknfeel; + diff --git a/kcmkwin/kwindecoration/qml/ButtonGroup.qml b/kcmkwin/kwindecoration/qml/ButtonGroup.qml new file mode 100644 index 0000000..a8f8ed5 --- /dev/null +++ b/kcmkwin/kwindecoration/qml/ButtonGroup.qml @@ -0,0 +1,75 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import QtQuick 2.1 +import QtQuick.Controls 1.2 +import QtQuick.Layouts 1.1 +import org.kde.kwin.private.kdecoration 1.0 as KDecoration +import org.kde.plasma.core 2.0 as PlasmaCore; + +ListView { + id: view + property string key + property bool dragging: false + orientation: ListView.Horizontal + interactive: false + spacing: units.smallSpacing + implicitHeight: units.iconSizes.small + implicitWidth: count * (units.iconSizes.small + units.smallSpacing) - Math.min(1, count) * units.smallSpacing + delegate: Item { + width: units.iconSizes.small + height: units.iconSizes.small + KDecoration.Button { + id: button + property int itemIndex: index + property var buttonsModel: parent.ListView.view.model + bridge: bridgeItem.bridge + settings: settingsItem + type: model["button"] + anchors.fill: Drag.active ? undefined : parent + Drag.keys: [ "decoButtonRemove", view.key ] + Drag.active: dragArea.drag.active + Drag.onActiveChanged: view.dragging = Drag.active + } + MouseArea { + id: dragArea + cursorShape: Qt.PointingHandCursor + 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/qml/Buttons.qml b/kcmkwin/kwindecoration/qml/Buttons.qml new file mode 100644 index 0000000..ce1bce9 --- /dev/null +++ b/kcmkwin/kwindecoration/qml/Buttons.qml @@ -0,0 +1,228 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import QtQuick 2.3 +import QtQuick.Controls 1.2 +import QtQuick.Controls 2.0 as QQC2 +import QtQuick.Layouts 1.1 +import org.kde.kwin.private.kdecoration 1.0 as KDecoration +import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddons; + +Item { + objectName: "buttonLayout" + Layout.preferredHeight: layout.height + KDecoration.Bridge { + id: bridgeItem + plugin: "org.kde.breeze" + } + KDecoration.Settings { + id: settingsItem + bridge: bridgeItem.bridge + } + Rectangle { + anchors.fill: parent + anchors.topMargin: units.gridUnit / 2 + border.width: Math.ceil(units.gridUnit / 16.0) + color: backgroundColor; + border.color: highlightColor; + ColumnLayout { + id: layout + width: parent.width + height: titlebarRect.height + availableGrid.height + dragHint.height + 5*layout.spacing + Rectangle { + id: titlebarRect + height: buttonPreviewRow.height + units.smallSpacing + Layout.fillWidth: true + border.width: Math.ceil(units.gridUnit / 16.0) + border.color: highlightColor + color: backgroundColor; + RowLayout { + id: buttonPreviewRow + anchors.top: parent.top; + anchors.left: parent.left; + anchors.right: parent.right; + anchors.margins: units.smallSpacing / 2 + height: Math.max(units.iconSizes.small, titlebar.implicitHeight) + units.smallSpacing/2 + ButtonGroup { + id: leftButtonsView + height: buttonPreviewRow.height + model: leftButtons + key: "decoButtonLeft" + } + QQC2.Label { + id: titlebar + Layout.fillWidth: true + horizontalAlignment: Text.AlignHCenter + font: titleFont + text: i18n("Titlebar") + } + + ButtonGroup { + id: rightButtonsView + height: buttonPreviewRow.height + model: rightButtons + key: "decoButtonRight" + } + } + DropArea { + anchors.fill: buttonPreviewRow + 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 = view.indexAt(point.x, point.y); + if (index == -1 && (view.x + view.width <= drag.x)) { + index = view.count - 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 right view + view.model.add(index, drag.source.type); + drag.source.buttonsModel.remove(drag.source.itemIndex); + } + } + } + } + } + Text { + id: iCannotBelieveIDoThis + text: "gnarf" + visible: false + } + GridView { + id: availableGrid + Layout.fillWidth: true + model: availableButtons + interactive: false + cellWidth: iconLabel.implicitWidth + cellHeight: units.iconSizes.small + iCannotBelieveIDoThis.implicitHeight + 4*units.smallSpacing + height: Math.ceil(cellHeight * 2.5) + delegate: Item { + id: availableDelegate + width: availableGrid.cellWidth + height: availableGrid.cellHeight + opacity: (leftButtonsView.dragging || rightButtonsView.dragging) ? 0.25 : 1.0 + KDecoration.Button { + id: availableButton + anchors.centerIn: Drag.active ? undefined : parent + bridge: bridgeItem.bridge + settings: settingsItem + type: model["button"] + width: units.iconSizes.small + height: units.iconSizes.small + Drag.keys: [ "decoButtonAdd" ] + Drag.active: dragArea.drag.active + } + QQC2.Label { + id: iconLabel + text: model["display"] + horizontalAlignment: Text.AlignHCenter + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right + elide: Text.ElideRight + wrapMode: Text.NoWrap + } + MouseArea { + id: dragArea + anchors.fill: parent + drag.target: availableButton + cursorShape: Qt.PointingHandCursor + onReleased: { + if (availableButton.Drag.target) { + availableButton.Drag.drop(); + } else { + availableButton.Drag.cancel(); + } + } + } + } + DropArea { + anchors.fill: parent + keys: [ "decoButtonRemove" ] + onEntered: { + drag.accept(); + } + onDropped: { + drag.source.buttonsModel.remove(drag.source.itemIndex); + } + ColumnLayout { + anchors.centerIn: parent + visible: leftButtonsView.dragging || rightButtonsView.dragging + QQC2.Label { + text: i18n("Drop here to remove button") + font.weight: Font.Bold + } + KQuickControlsAddons.QIconItem { + id: icon + width: 64 + height: 64 + icon: "list-remove" + Layout.alignment: Qt.AlignHCenter + } + Item { + Layout.fillHeight: true + } + } + } + } + Text { + id: dragHint + visible: !(leftButtonsView.dragging || rightButtonsView.dragging || availableGrid.dragging) + Layout.fillWidth: true + color: backgroundColor; + opacity: 0.66 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignTop + text: i18n("Drag buttons between here and the titlebar") + } + } + } +} diff --git a/kcmkwin/kwindecoration/qml/Previews.qml b/kcmkwin/kwindecoration/qml/Previews.qml new file mode 100644 index 0000000..0916920 --- /dev/null +++ b/kcmkwin/kwindecoration/qml/Previews.qml @@ -0,0 +1,151 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import QtQuick 2.1 +import QtQuick.Controls 1.2 +import QtQuick.Layouts 1.1 +import org.kde.kwin.private.kdecoration 1.0 as KDecoration + +ScrollView { + objectName: "themeList" + frameVisible: true + GridView { + id: gridView + objectName: "listView" + model: decorationsModel + cellWidth: 20 * units.gridUnit + cellHeight: cellWidth / 1.6 + onContentHeightChanged: { + if (gridView.currentIndex == -1) { + gridView.currentIndex = savedIndex; + } + gridView.positionViewAtIndex(gridView.currentIndex, GridView.Visible); + } + + Rectangle { + z: -1 + anchors.fill: parent + color: baseColor + } + highlight: Rectangle { + color: highlightColor + opacity: 0.6 + } + highlightMoveDuration: units.longDuration + boundsBehavior: Flickable.StopAtBounds + property int borderSizesIndex: 3 // 3 == Normal + delegate: Item { + width: gridView.cellWidth + height: gridView.cellHeight + KDecoration.Bridge { + id: bridgeItem + plugin: model["plugin"] + theme: model["theme"] + } + KDecoration.Settings { + id: settingsItem + bridge: bridgeItem.bridge + borderSizesIndex: gridView.borderSizesIndex + } + MouseArea { + hoverEnabled: false + anchors.fill: parent + onClicked: { + gridView.currentIndex = index; + } + } + ColumnLayout { + id: decorationPreviews + property string themeName: model["display"] + anchors.fill: parent + Item { + KDecoration.Decoration { + id: inactivePreview + bridge: bridgeItem.bridge + settings: settingsItem + anchors.fill: parent + Component.onCompleted: { + client.caption = decorationPreviews.themeName + client.active = false; + anchors.leftMargin = Qt.binding(function() { return 40 - (inactivePreview.shadow ? inactivePreview.shadow.paddingLeft : 0);}); + anchors.rightMargin = Qt.binding(function() { return 10 - (inactivePreview.shadow ? inactivePreview.shadow.paddingRight : 0);}); + anchors.topMargin = Qt.binding(function() { return 10 - (inactivePreview.shadow ? inactivePreview.shadow.paddingTop : 0);}); + anchors.bottomMargin = Qt.binding(function() { return 40 - (inactivePreview.shadow ? inactivePreview.shadow.paddingBottom : 0);}); + } + } + KDecoration.Decoration { + id: activePreview + bridge: bridgeItem.bridge + settings: settingsItem + anchors.fill: parent + Component.onCompleted: { + client.caption = decorationPreviews.themeName + client.active = true; + anchors.leftMargin = Qt.binding(function() { return 10 - (activePreview.shadow ? activePreview.shadow.paddingLeft : 0);}); + anchors.rightMargin = Qt.binding(function() { return 40 - (activePreview.shadow ? activePreview.shadow.paddingRight : 0);}); + anchors.topMargin = Qt.binding(function() { return 40 - (activePreview.shadow ? activePreview.shadow.paddingTop : 0);}); + anchors.bottomMargin = Qt.binding(function() { return 10 - (activePreview.shadow ? activePreview.shadow.paddingBottom : 0);}); + } + } + MouseArea { + hoverEnabled: false + anchors.fill: parent + onClicked: { + gridView.currentIndex = index; + } + } + Layout.fillWidth: true + Layout.fillHeight: true + /* Create an invisible rectangle that's the same size as the inner content rect + of the foreground window preview so that we can center the button within it. + We have to center rather than using anchors because the button width varies + with different localizations */ + Item { + anchors { + left: parent.left + leftMargin: 10 + right: parent.right + rightMargin: 40 + top: parent.top + topMargin: 68 + bottom: parent.bottom + bottomMargin: 10 + } + Button { + id: configureButton + anchors { + horizontalCenter: parent.horizontalCenter + verticalCenter: parent.verticalCenter + } + enabled: model["configureable"] + iconName: "configure" + text: i18n("Configure %1...", decorationPreviews.themeName) + onClicked: { + gridView.currentIndex = index + bridgeItem.bridge.configure() + } + } + } + } + } + } + } + Layout.preferredHeight: 20 * units.gridUnit +} + diff --git a/kcmkwin/kwindecoration/qml/main.qml b/kcmkwin/kwindecoration/qml/main.qml new file mode 100644 index 0000000..75a754e --- /dev/null +++ b/kcmkwin/kwindecoration/qml/main.qml @@ -0,0 +1,37 @@ +/* + * Copyright 2014 Martin Gräßlin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +import QtQuick 2.1 +import QtQuick.Controls 1.2 +import QtQuick.Layouts 1.1 + +Rectangle { + color: backgroundColor + ColumnLayout { + anchors.fill: parent + Previews { + Layout.fillWidth: true + Layout.fillHeight: true + } + Buttons { + Layout.alignment: Qt.AlignTop + Layout.fillWidth: true + } + } +} diff --git a/kcmkwin/kwindesktop/CMakeLists.txt b/kcmkwin/kwindesktop/CMakeLists.txt new file mode 100644 index 0000000..77572be --- /dev/null +++ b/kcmkwin/kwindesktop/CMakeLists.txt @@ -0,0 +1,33 @@ + +########### next target ############### +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcm_kwindesktop\") + +include_directories(${KWIN_SOURCE_DIR}/effects) + +set(kcm_kwindesktop_PART_SRCS main.cpp desktopnameswidget.cpp) +ki18n_wrap_ui(kcm_kwindesktop_PART_SRCS main.ui) +qt5_add_dbus_interface( kcm_kwindesktop_PART_SRCS + ${KWIN_SOURCE_DIR}/org.kde.kwin.Effects.xml kwin_effects_interface) + +add_library(kcm_kwindesktop MODULE ${kcm_kwindesktop_PART_SRCS}) + +target_link_libraries(kcm_kwindesktop + Qt5::X11Extras + KF5::KCMUtils + KF5::Completion + KF5::GlobalAccel + KF5::I18n + KF5::Package + KF5::WindowSystem + KF5::XmlGui + ${X11_LIBRARIES} + kwin4_effect_builtins +) + +install(TARGETS kcm_kwindesktop DESTINATION ${PLUGIN_INSTALL_DIR} ) + + +########### install files ############### +install( FILES desktop.desktop DESTINATION ${SERVICES_INSTALL_DIR} ) + diff --git a/kcmkwin/kwindesktop/Messages.sh b/kcmkwin/kwindesktop/Messages.sh new file mode 100644 index 0000000..83e1bb1 --- /dev/null +++ b/kcmkwin/kwindesktop/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC `find . -name \*.ui` >> rc.cpp || exit 11 +$XGETTEXT *.cpp -o $podir/kcm_kwindesktop.pot +rm -f rc.cpp diff --git a/kcmkwin/kwindesktop/desktop.desktop b/kcmkwin/kwindesktop/desktop.desktop new file mode 100644 index 0000000..a5aaee8 --- /dev/null +++ b/kcmkwin/kwindesktop/desktop.desktop @@ -0,0 +1,161 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule +X-DocPath=kcontrol/desktop/index.html +Icon=preferences-desktop +Exec=kcmshell5 desktop + +X-KDE-Library=kcm_kwindesktop +X-KDE-ParentApp=kcontrol + +X-KDE-System-Settings-Parent-Category=desktopbehavior +X-KDE-Weight=60 + +Name=Virtual Desktops +Name[ar]=أسطح المكتب الافتراضية +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]=Virtual Desktops +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[tg]=Мизҳои кории виртуалӣ +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=Navigation, Number and Layout of Virtual Desktops +Comment[bs]=Navigacija, broj i izgled virtualnih desktopa +Comment[ca]=Navegació, nombre i disposició dels escriptoris virtuals +Comment[ca@valencia]=Navegació, nombre i disposició dels escriptoris virtuals +Comment[cs]=Navigace, počet a rozvržení virtuálních ploch +Comment[da]=Navigation, antal og layout af virtuelle skriveborde +Comment[de]=Navigation, Anzahl und Layout virtueller Arbeitsflächen +Comment[el]=Περιήγηση, αριθμός και διάταξη εικονικών επιφανειών εργασίας +Comment[en_GB]=Navigation, Number and Layout of Virtual Desktops +Comment[es]=Navegación, número y disposición de los escritorios virtuales +Comment[et]=Virtuaalsete töölaudade vahel liikumine, nende arv ja paigutus +Comment[eu]=Nabigazioa, alegiazko mahaigainen kopurua eta antolamendua +Comment[fi]=Virtuaalityöpöytien vaihtaminen, määrä ja asettelu +Comment[fr]=Navigation, nombre et disposition des bureaux virtuels +Comment[gl]=Navegación, cantidade e disposición dos escritorios virtuais +Comment[he]=ניווט, פריסה ומספר שולחנות עבודה וירטואלים +Comment[hu]=Navigáció, a virtuális asztalok száma és elrendezése +Comment[id]=Navigasi, Jumlah dan Tata Letak Desktop Virtual +Comment[it]=Navigazione, numero e disposizione dei desktop virtuali +Comment[ko]=가상 데스크톱 탐색, 개수, 레이아웃 +Comment[lt]=Naršymas, Skaičius ir išdėstymas virtualių darbalaukių +Comment[nb]=Navigering, antall og utlegg av virtuelle skrivebord +Comment[nds]=Tall, Anornen un dat Anstüern vun de virtuellen Schriefdischen fastleggen +Comment[nl]=Navigatie door, aantal en indeling van virtuele bureaubladen +Comment[nn]=Navigering, nummer og vising av virtuelle skrivebord +Comment[pa]=ਵਰਚੁਅਲ ਡੈਸਕਟਾਪਾਂ ਲਈ ਨੇਵੀਗੇਸ਼ਨ, ਗਿਣਤੀ ਅਤੇ ਢਾਂਚਾ +Comment[pl]=Poruszanie się, liczba i układ wirtualnych pulpitów +Comment[pt]=Navegação, Número e Disposição dos Ecrãs Virtuais +Comment[pt_BR]=Navegação, quantidade e layout das áreas de trabalho virtuais +Comment[ru]=Число, расположение и способ переключения рабочих столов +Comment[sk]=Navigácia, počet a rozloženie virtuálnych plôch +Comment[sl]=Krmarjenje med, število in razporeditev navideznih namizij +Comment[sr]=Кретање, број и распоред виртуелних површи +Comment[sr@ijekavian]=Кретање, број и распоред виртуелних површи +Comment[sr@ijekavianlatin]=Kretanje, broj i raspored virtuelnih površi +Comment[sr@latin]=Kretanje, broj i raspored virtuelnih površi +Comment[sv]=Navigering, antal och layout av virtuella skrivbord +Comment[tr]=Gezinti, Sanal Masaüstlerinin Sayısı ve Yerleşimi +Comment[uk]=Навігація, кількість та компонування віртуальних стільниць +Comment[vi]=Số lượng, bố trí và điều hướng của màn hình ảo +Comment[x-test]=xxNavigation, 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[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,escriptoris múltiples,paginador,estri paginador,miniaplicació de paginació,arranjament de 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,setelan 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[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[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 diff --git a/kcmkwin/kwindesktop/desktopnameswidget.cpp b/kcmkwin/kwindesktop/desktopnameswidget.cpp new file mode 100644 index 0000000..b010782 --- /dev/null +++ b/kcmkwin/kwindesktop/desktopnameswidget.cpp @@ -0,0 +1,124 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#include "desktopnameswidget.h" +#include "main.h" + +#include +#include + +#include +#include + +namespace KWin +{ + +DesktopNamesWidget::DesktopNamesWidget(QWidget *parent) + : QWidget(parent) + , m_maxDesktops(0) + , m_desktopConfig(0) +{ + m_namesLayout = new QGridLayout; + m_namesLayout->setMargin(0); + + setLayout(m_namesLayout); +} + +DesktopNamesWidget::~DesktopNamesWidget() +{ +} + +void DesktopNamesWidget::numberChanged(int number) +{ + if ((number < 1) || (number > m_maxDesktops)) + return; + if (m_nameInputs.size() != number) { + if (number < m_nameInputs.size()) { + // remove widgets + while (number != m_nameInputs.size()) { + KLineEdit* edit = m_nameInputs.last(); + m_nameInputs.removeLast(); + delete edit; + QLabel* label = m_nameLabels.last(); + m_nameLabels.removeLast(); + delete label; + } + } else { + // add widgets + while (number != m_nameInputs.size()) { + int desktop = m_nameInputs.size(); + QLabel* label = new QLabel(i18n("Desktop %1:", desktop + 1), this); + KLineEdit* edit = new KLineEdit(this); + label->setWhatsThis(i18n("Here you can enter the name for desktop %1", desktop + 1)); + edit->setWhatsThis(i18n("Here you can enter the name for desktop %1", desktop + 1)); + + m_namesLayout->addWidget(label, desktop % 10, 0 + 2 *(desktop >= 10), 1, 1); + m_namesLayout->addWidget(edit, desktop % 10, 1 + 2 *(desktop >= 10), 1, 1); + m_nameInputs << edit; + m_nameLabels << label; + + setDefaultName(desktop + 1); + if (desktop > 1) { + setTabOrder(m_nameInputs[desktop - 1], m_nameInputs[desktop]); + } + connect(edit, SIGNAL(textChanged(QString)), SIGNAL(changed())); + } + } + } +} + +QString DesktopNamesWidget::name(int desktop) +{ + if ((desktop < 1) || (desktop > m_maxDesktops) || (desktop > m_nameInputs.size())) + return QString(); + return m_nameInputs[ desktop -1 ]->text(); +} + + +void DesktopNamesWidget::setName(int desktop, QString desktopName) +{ + if ((desktop < 1) || (desktop > m_maxDesktops) || (desktop > m_nameInputs.size())) + return; + m_nameInputs[ desktop-1 ]->setText(desktopName); +} + +void DesktopNamesWidget::setDefaultName(int desktop) +{ + if ((desktop < 1) || (desktop > m_maxDesktops)) + return; + QString name = m_desktopConfig->cachedDesktopName(desktop); + if (name.isEmpty()) + name = i18n("Desktop %1", desktop); + m_nameInputs[ desktop -1 ]->setText(name); +} + + +void DesktopNamesWidget::setMaxDesktops(int maxDesktops) +{ + m_maxDesktops = maxDesktops; +} + +void DesktopNamesWidget::setDesktopConfig(KWinDesktopConfig* desktopConfig) +{ + m_desktopConfig = desktopConfig; +} + +} // namespace + diff --git a/kcmkwin/kwindesktop/desktopnameswidget.h b/kcmkwin/kwindesktop/desktopnameswidget.h new file mode 100644 index 0000000..4507103 --- /dev/null +++ b/kcmkwin/kwindesktop/desktopnameswidget.h @@ -0,0 +1,63 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef DESKTOPNAMESWIDGET_H +#define DESKTOPNAMESWIDGET_H + +#include +#include + +class KLineEdit; +class QLabel; +class QGridLayout; + +namespace KWin +{ +class KWinDesktopConfig; + +class DesktopNamesWidget : public QWidget +{ + Q_OBJECT +public: + explicit DesktopNamesWidget(QWidget *parent); + ~DesktopNamesWidget(); + QString name(int desktop); + void setName(int desktop, QString desktopName); + void setDefaultName(int desktop); + void setMaxDesktops(int maxDesktops); + void setDesktopConfig(KWinDesktopConfig *desktopConfig); + +Q_SIGNALS: + void changed(); + +public Q_SLOTS: + void numberChanged(int number); + +private: + QList< QLabel* > m_nameLabels; + QList< KLineEdit* > m_nameInputs; + QGridLayout* m_namesLayout; + int m_maxDesktops; + KWinDesktopConfig *m_desktopConfig; +}; + +} // namespace + +#endif // DESKTOPNAMESWIDGET_H diff --git a/kcmkwin/kwindesktop/main.cpp b/kcmkwin/kwindesktop/main.cpp new file mode 100644 index 0000000..31b8e2e --- /dev/null +++ b/kcmkwin/kwindesktop/main.cpp @@ -0,0 +1,669 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#include "main.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 + +K_PLUGIN_FACTORY(KWinDesktopConfigFactory, registerPlugin();) + +namespace KWin +{ + +KWinDesktopConfigForm::KWinDesktopConfigForm(QWidget* parent) + : QWidget(parent) +{ + setupUi(this); +} + +KWinDesktopConfig::KWinDesktopConfig(QWidget* parent, const QVariantList& args) + : KCModule(KAboutData::pluginData(QStringLiteral("kcm_kwindesktop")), parent, args) + , m_config(KSharedConfig::openConfig("kwinrc")) + , m_actionCollection(nullptr) + , m_switchDesktopCollection(nullptr) +{ + init(); +} + +void KWinDesktopConfig::init() +{ + m_ui = new KWinDesktopConfigForm(this); + // TODO: there has to be a way to add the shortcuts editor to the ui file + m_editor = new KShortcutsEditor(m_ui, KShortcutsEditor::GlobalAction); + m_ui->editorFrame->setLayout(new QVBoxLayout()); + m_ui->editorFrame->layout()->setMargin(0); + m_ui->editorFrame->layout()->addWidget(m_editor); + + m_ui->desktopNames->setDesktopConfig(this); + m_ui->desktopNames->setMaxDesktops(maxDesktops); + m_ui->desktopNames->numberChanged(defaultDesktops); + + QVBoxLayout* layout = new QVBoxLayout(this); + layout->addWidget(m_ui); + + setQuickHelp(i18n("

Multiple Desktops

In this module, you can configure how many virtual desktops you want and how these should be labeled.")); + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + m_actionCollection->setConfigGroup("Desktop Switching"); + m_actionCollection->setConfigGlobal(true); + + m_switchDesktopCollection = new KActionCollection(this, QStringLiteral("kwin")); + m_switchDesktopCollection->setConfigGroup("Desktop Switching"); + m_switchDesktopCollection->setConfigGlobal(true); + + // actions for switch desktop collection - other action is filled dynamically + addAction("Switch to Next Desktop", i18n("Switch to Next Desktop")); + addAction("Switch to Previous Desktop", i18n("Switch to Previous Desktop")); + addAction("Switch One Desktop to the Right", i18n("Switch One Desktop to the Right")); + addAction("Switch One Desktop to the Left", i18n("Switch One Desktop to the Left")); + addAction("Switch One Desktop Up", i18n("Switch One Desktop Up")); + addAction("Switch One Desktop Down", i18n("Switch One Desktop Down")); + addAction("Walk Through Desktops", i18n("Walk Through Desktops")); + addAction("Walk Through Desktops (Reverse)", i18n("Walk Through Desktops (Reverse)")); + addAction("Walk Through Desktop List", i18n("Walk Through Desktop List")); + addAction("Walk Through Desktop List (Reverse)", i18n("Walk Through Desktop List (Reverse)")); + + m_editor->addCollection(m_switchDesktopCollection, i18n("Desktop Switching")); + + // get number of desktops + int n = 1; + if (QX11Info::isPlatformX11()) { + NETRootInfo info(QX11Info::connection(), NET::NumberOfDesktops | NET::DesktopNames); + n = info.numberOfDesktops(); + } + + auto addSwitchTo = [this](int i, const QKeySequence &sequence) { + QAction* a = m_actionCollection->addAction(QString("Switch to Desktop %1").arg(i)); + a->setProperty("isConfigurationAction", true); + a->setText(i18n("Switch to Desktop %1", i)); + KGlobalAccel::setGlobalShortcut(a, sequence); + }; + if (n >= 2) { + addSwitchTo(1, Qt::CTRL + Qt::Key_F1); + addSwitchTo(2, Qt::CTRL + Qt::Key_F2); + } + if (n >= 3) { + addSwitchTo(3, Qt::CTRL + Qt::Key_F3); + } + if (n >= 4) { + addSwitchTo(4, Qt::CTRL + Qt::Key_F4); + } + for (int i = 5; i <= n; ++i) { + addSwitchTo(i, QKeySequence()); + } + + // This should be after the "Switch to Desktop %1" loop. It HAS to be + // there after numberSpinBox is connected to slotChangeShortcuts. We would + // overwrite the users settings if not, + m_ui->numberSpinBox->setValue(n); + + m_editor->addCollection(m_actionCollection, i18n("Desktop Switching")); + + // search the effect names + // TODO: way to recognize if a effect is not found + KServiceTypeTrader* trader = KServiceTypeTrader::self(); + QString fadedesktop; + KService::List services = trader->query("KWin/Effect", "[X-KDE-PluginInfo-Name] == 'kwin4_effect_fadedesktop'"); + if (!services.isEmpty()) + fadedesktop = services.first()->name(); + + m_ui->effectComboBox->addItem(i18n("No Animation")); + m_ui->effectComboBox->addItem(BuiltInEffects::effectData(BuiltInEffect::Slide).displayName); + m_ui->effectComboBox->addItem(BuiltInEffects::effectData(BuiltInEffect::CubeSlide).displayName); + m_ui->effectComboBox->addItem(fadedesktop); + + // effect config and info button + m_ui->effectInfoButton->setIcon(QIcon::fromTheme("dialog-information")); + m_ui->effectConfigButton->setIcon(QIcon::fromTheme("configure")); + + connect(m_ui->rowsSpinBox, SIGNAL(valueChanged(int)), SLOT(changed())); + connect(m_ui->numberSpinBox, SIGNAL(valueChanged(int)), SLOT(changed())); + connect(m_ui->numberSpinBox, SIGNAL(valueChanged(int)), SLOT(slotChangeShortcuts(int))); + connect(m_ui->desktopNames, SIGNAL(changed()), SLOT(changed())); + connect(m_ui->popupInfoCheckBox, SIGNAL(toggled(bool)), SLOT(changed())); + connect(m_ui->popupHideSpinBox, SIGNAL(valueChanged(int)), SLOT(changed())); + connect(m_ui->desktopLayoutIndicatorCheckBox, SIGNAL(stateChanged(int)), SLOT(changed())); + connect(m_ui->wrapAroundBox, SIGNAL(stateChanged(int)), SLOT(changed())); + connect(m_editor, SIGNAL(keyChange()), SLOT(changed())); + connect(m_ui->allShortcutsCheckBox, SIGNAL(stateChanged(int)), SLOT(slotShowAllShortcuts())); + connect(m_ui->effectComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changed())); + connect(m_ui->effectComboBox, SIGNAL(currentIndexChanged(int)), SLOT(slotEffectSelectionChanged(int))); + connect(m_ui->effectInfoButton, SIGNAL(clicked()), SLOT(slotAboutEffectClicked())); + connect(m_ui->effectConfigButton, SIGNAL(clicked()), SLOT(slotConfigureEffectClicked())); + + // Begin check for immutable - taken from old desktops kcm + int kwin_screen_number = QX11Info::appScreen(); + + m_config = KSharedConfig::openConfig("kwinrc"); + + QByteArray groupname; + if (kwin_screen_number == 0) + groupname = "Desktops"; + else + groupname = "Desktops-screen-" + QByteArray::number(kwin_screen_number); + + if (m_config->isGroupImmutable(groupname)) { + m_ui->nameGroup->setEnabled(false); + //number of desktops widgets + m_ui->numberLabel->setEnabled(false); + m_ui->numberSpinBox->setEnabled(false); + m_ui->rowsSpinBox->setEnabled(false); + } else { + KConfigGroup cfgGroup(m_config.data(), groupname.constData()); + if (cfgGroup.isEntryImmutable("Number")) { + //number of desktops widgets + m_ui->numberLabel->setEnabled(false); + m_ui->numberSpinBox->setEnabled(false); + m_ui->rowsSpinBox->setEnabled(false); + } + } + // End check for immutable +} + +KWinDesktopConfig::~KWinDesktopConfig() +{ + undo(); +} + +void KWinDesktopConfig::addAction(const QString &name, const QString &label) +{ + QAction* a = m_switchDesktopCollection->addAction(name); + a->setProperty("isConfigurationAction", true); + a->setText(label); + KGlobalAccel::setGlobalShortcut(a, QKeySequence()); +} + +void KWinDesktopConfig::defaults() +{ + // TODO: plasma stuff + m_ui->numberSpinBox->setValue(defaultDesktops); + m_ui->desktopNames->numberChanged(defaultDesktops); + for (int i = 1; i <= maxDesktops; i++) { + m_desktopNames[i-1] = i18n("Desktop %1", i); + if (i <= defaultDesktops) + m_ui->desktopNames->setDefaultName(i); + } + + // popup info + m_ui->popupInfoCheckBox->setChecked(false); + m_ui->popupHideSpinBox->setValue(1000); + m_ui->desktopLayoutIndicatorCheckBox->setChecked(true); + + m_ui->effectComboBox->setCurrentIndex(1); + + m_ui->wrapAroundBox->setChecked(true); + + m_ui->rowsSpinBox->setValue(2); + + m_editor->allDefault(); + + emit changed(true); +} + + +void KWinDesktopConfig::load() +{ + // This method is called on reset(). So undo all changes. + undo(); + + if (QX11Info::isPlatformX11()) { + // get number of desktops + NETRootInfo info(QX11Info::connection(), NET::NumberOfDesktops | NET::DesktopNames, NET::WM2DesktopLayout); + + for (int i = 1; i <= maxDesktops; i++) { + QString name = QString::fromUtf8(info.desktopName(i)); + m_desktopNames << name; + m_ui->desktopNames->setName(i, name); + } + m_ui->rowsSpinBox->setValue(info.desktopLayoutColumnsRows().height()); + } else { + // TODO: proper implementation + m_ui->rowsSpinBox->setValue(1); + } + + // Popup info + KConfigGroup effectconfig(m_config, "Plugins"); + KConfigGroup popupInfo(m_config, "Script-desktopchangeosd"); + m_ui->popupInfoCheckBox->setChecked(effectconfig.readEntry("desktopchangeosdEnabled", false)); + m_ui->popupHideSpinBox->setValue(popupInfo.readEntry("PopupHideDelay", 1000)); + m_ui->desktopLayoutIndicatorCheckBox->setChecked(!popupInfo.readEntry("TextOnly", false)); + + // Wrap Around on screen edge + KConfigGroup windowConfig(m_config, "Windows"); + m_ui->wrapAroundBox->setChecked(windowConfig.readEntry("RollOverDesktops", true)); + + // Effect for desktop switching + // Set current option to "none" if no plugin is activated. + m_ui->effectComboBox->setCurrentIndex(0); + auto enableBuiltInEffect = [&effectconfig,this](BuiltInEffect effect, int index) { + const QString key = BuiltInEffects::nameForEffect(effect) + QStringLiteral("Enabled"); + if (effectconfig.readEntry(key, BuiltInEffects::enabledByDefault(effect))) { + m_ui->effectComboBox->setCurrentIndex(index); + } + }; + enableBuiltInEffect(BuiltInEffect::Slide, 1); + enableBuiltInEffect(BuiltInEffect::CubeSlide, 2); + if (effectEnabled("fadedesktop", effectconfig)) + m_ui->effectComboBox->setCurrentIndex(3); + slotEffectSelectionChanged(m_ui->effectComboBox->currentIndex()); + // TODO: plasma stuff + + emit changed(false); +} + +void KWinDesktopConfig::save() +{ + // TODO: plasma stuff + + const int numberDesktops = m_ui->numberSpinBox->value(); + int rows = m_ui->rowsSpinBox->value(); + rows = qBound(1, rows, numberDesktops); + // avoid weird cases like having 3 rows for 4 desktops, where the last row is unused + int columns = numberDesktops / rows; + if (numberDesktops % rows > 0) { + columns++; + } + + if (QX11Info::isPlatformX11()) { + NETRootInfo info(QX11Info::connection(), NET::NumberOfDesktops | NET::DesktopNames, NET::WM2DesktopLayout); + // set desktop names + for (int i = 1; i <= maxDesktops; i++) { + QString desktopName = m_desktopNames[ i -1 ]; + if (i <= m_ui->numberSpinBox->value()) + desktopName = m_ui->desktopNames->name(i); + info.setDesktopName(i, desktopName.toUtf8()); + info.activate(); + } + // set number of desktops + info.setNumberOfDesktops(numberDesktops); + info.activate(); + info.setDesktopLayout(NET::OrientationHorizontal, columns, rows, NET::DesktopLayoutCornerTopLeft); + info.activate(); + + XSync(QX11Info::display(), false); + } + + // save the desktops + QString groupname; + const int screenNumber = QX11Info::appScreen(); + if (screenNumber == 0) + groupname = "Desktops"; + else + groupname.sprintf("Desktops-screen-%d", screenNumber); + KConfigGroup group(m_config, groupname); + group.writeEntry("Rows", rows); + + // Popup info + KConfigGroup effectconfig(m_config, "Plugins"); + KConfigGroup popupInfo(m_config, "Script-desktopchangeosd"); + effectconfig.writeEntry("desktopchangeosdEnabled", m_ui->popupInfoCheckBox->isChecked()); + popupInfo.writeEntry("PopupHideDelay", m_ui->popupHideSpinBox->value()); + popupInfo.writeEntry("TextOnly", !m_ui->desktopLayoutIndicatorCheckBox->isChecked()); + + // Wrap Around on screen edge + KConfigGroup windowConfig(m_config, "Windows"); + windowConfig.writeEntry("RollOverDesktops", m_ui->wrapAroundBox->isChecked()); + + // Effect desktop switching + int desktopSwitcher = m_ui->effectComboBox->currentIndex(); + bool slideEnabled = false; + bool cubeSlideEnabled = false; + bool fadeEnabled = false; + switch(desktopSwitcher) { + case 1: + // slide + slideEnabled = true; + break; + case 2: + // cube + cubeSlideEnabled = true; + break; + case 3: + // fadedesktop + fadeEnabled = true; + break; + } + + effectconfig.writeEntry("slideEnabled", slideEnabled); + effectconfig.writeEntry("cubeslideEnabled", cubeSlideEnabled); + effectconfig.writeEntry("kwin4_effect_fadedesktopEnabled", fadeEnabled); + + m_editor->save(); + + m_config->sync(); + // Send signal to all kwin instances + 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()); + if (slideEnabled) { + interface.loadEffect(BuiltInEffects::nameForEffect(BuiltInEffect::Slide)); + } else { + interface.unloadEffect(BuiltInEffects::nameForEffect(BuiltInEffect::Slide)); + } + if (cubeSlideEnabled) { + interface.loadEffect(BuiltInEffects::nameForEffect(BuiltInEffect::CubeSlide)); + } else { + interface.unloadEffect(BuiltInEffects::nameForEffect(BuiltInEffect::CubeSlide)); + } + if (fadeEnabled) { + interface.loadEffect(QStringLiteral("kwin4_effect_fadedesktop")); + } else { + interface.unloadEffect(QStringLiteral("kwin4_effect_fadedesktop")); + } + + emit changed(false); +} + + +void KWinDesktopConfig::undo() +{ + // The global shortcuts editor makes changes active immediately. In case + // of undo we have to undo them manually + m_editor->undoChanges(); +} + +QString KWinDesktopConfig::cachedDesktopName(int desktop) +{ + if (desktop > m_desktopNames.size()) + return QString(); + return m_desktopNames[ desktop -1 ]; +} + +QString KWinDesktopConfig::extrapolatedShortcut(int desktop) const +{ + + if (!desktop || desktop > m_actionCollection->count()) + return QString(); + if (desktop == 1) + return QString("Ctrl+F1"); + + QAction *beforeAction = m_actionCollection->actions().at(qMin(9, desktop - 2)); + auto shortcuts = KGlobalAccel::self()->shortcut(beforeAction); + if (shortcuts.isEmpty()) { + shortcuts = KGlobalAccel::self()->defaultShortcut(beforeAction); + } + QString before; + if (!shortcuts.isEmpty()) { + before = shortcuts.first().toString(QKeySequence::PortableText); + } + + QString seq; + if (before.contains(QRegExp("F[0-9]{1,2}"))) { + if (desktop < 13) // 10? + seq = QString("F%1").arg(desktop); + else if (!before.contains("Shift")) + seq = "Shift+" + QString("F%1").arg(desktop - 10); + } else if (before.contains(QRegExp("[0-9]"))) { + if (desktop == 10) + seq = '0'; + else if (desktop > 10) { + if (!before.contains("Shift")) + seq = "Shift+" + QString::number(desktop == 20 ? 0 : (desktop - 10)); + } else + seq = QString::number(desktop); + } + + if (!seq.isEmpty()) { + if (before.contains("Ctrl")) + seq.prepend("Ctrl+"); + if (before.contains("Alt")) + seq.prepend("Alt+"); + if (before.contains("Shift")) + seq.prepend("Shift+"); + if (before.contains("Meta")) + seq.prepend("Meta+"); + } + return seq; +} + +void KWinDesktopConfig::slotChangeShortcuts(int number) +{ + if ((number < 1) || (number > maxDesktops)) + return; + + if (m_ui->allShortcutsCheckBox->isChecked()) + number = maxDesktops; + + while (number != m_actionCollection->count()) { + if (number < m_actionCollection->count()) { + // Remove the action from the action collection. The action itself + // will still exist because that's the way kwin currently works. + // No need to remove/forget it. See kwinbindings. + QAction *a = m_actionCollection->takeAction(m_actionCollection->actions().last()); + // Remove any associated global shortcut. Set it to "" + KGlobalAccel::self()->setShortcut(a, QList(), KGlobalAccel::NoAutoloading); + m_ui->messageLabel->hide(); + delete a; + } else { + // add desktop + int desktop = m_actionCollection->count() + 1; + QAction* action = m_actionCollection->addAction(QString("Switch to Desktop %1").arg(desktop)); + action->setProperty("isConfigurationAction", true); + action->setText(i18n("Switch to Desktop %1", desktop)); + KGlobalAccel::self()->setShortcut(action, QList()); + QString shortcutString = extrapolatedShortcut(desktop); + if (shortcutString.isEmpty()) { + m_ui->messageLabel->setText(i18n("No suitable Shortcut for Desktop %1 found", desktop)); + m_ui->messageLabel->show(); + } else { + QKeySequence shortcut(shortcutString); + if (!shortcut.isEmpty() && KGlobalAccel::self()->isGlobalShortcutAvailable(shortcut)) { + KGlobalAccel::self()->setShortcut(action, QList() << shortcut, KGlobalAccel::NoAutoloading); + m_ui->messageLabel->setText(i18n("Assigned global Shortcut \"%1\" to Desktop %2", shortcutString, desktop)); + m_ui->messageLabel->show(); + } else { + m_ui->messageLabel->setText(i18n("Shortcut conflict: Could not set Shortcut %1 for Desktop %2", shortcutString, desktop)); + m_ui->messageLabel->show(); + } + } + } + } + m_editor->clearCollections(); + m_editor->addCollection(m_switchDesktopCollection, i18n("Desktop Switching")); + m_editor->addCollection(m_actionCollection, i18n("Desktop Switching")); +} + +void KWinDesktopConfig::slotShowAllShortcuts() +{ + slotChangeShortcuts(m_ui->numberSpinBox->value()); +} + +void KWinDesktopConfig::slotEffectSelectionChanged(int index) +{ + bool enabled = false; + if (index != 0) + enabled = true; + m_ui->effectInfoButton->setEnabled(enabled); + + switch (index) { + case 1: // Slide + case 2: // Cube Slide + enabled = true; + break; + default: + enabled = false; + break; + } + m_ui->effectConfigButton->setEnabled(enabled); +} + + +bool KWinDesktopConfig::effectEnabled(const QString& effect, const KConfigGroup& cfg) const +{ + KService::List services = KServiceTypeTrader::self()->query( + "KWin/Effect", "[X-KDE-PluginInfo-Name] == 'kwin4_effect_" + effect + '\''); + if (services.isEmpty()) + return false; + QVariant v = services.first()->property("X-KDE-PluginInfo-EnabledByDefault"); + return cfg.readEntry("kwin4_effect_" + effect + "Enabled", v.toBool()); +} + +void KWinDesktopConfig::slotAboutEffectClicked() +{ + QString effect; + bool fromKService = false; + BuiltInEffect builtIn = BuiltInEffect::Invalid; + switch(m_ui->effectComboBox->currentIndex()) { + case 1: + builtIn = BuiltInEffect::Slide; + break; + case 2: + builtIn = BuiltInEffect::CubeSlide; + break; + case 3: + effect = "fadedesktop"; + fromKService = true; + break; + default: + return; + } + auto showDialog = [this](const KAboutData &aboutData) { + QPointer aboutPlugin = new KAboutApplicationDialog(aboutData, this); + aboutPlugin->exec(); + delete aboutPlugin; + }; + if (fromKService) { + const QString pluginId = QStringLiteral("kwin4_effect_%1").arg(effect); + const auto effectsMetaData = KPackage::PackageLoader::self()->findPackages( + QStringLiteral("KWin/Effect"), + QStringLiteral("kwin/effects/"), + [&pluginId](const KPluginMetaData &meta) { + return meta.pluginId() == pluginId; + }); + if (effectsMetaData.isEmpty()) { + return; + } + KPluginInfo pluginInfo(effectsMetaData.first()); + + const QString name = pluginInfo.name(); + const QString comment = pluginInfo.comment(); + const QString author = pluginInfo.author(); + const QString email = pluginInfo.email(); + const QString website = pluginInfo.website(); + const QString version = pluginInfo.version(); + const QString license = pluginInfo.license(); + const QString icon = pluginInfo.icon(); + + KAboutData aboutData(name, name, version, comment, KAboutLicense::byKeyword(license).key(), QString(), QString(), website.toLatin1()); + aboutData.setProgramLogo(icon); + const QStringList authors = author.split(','); + const QStringList emails = email.split(','); + int i = 0; + if (authors.count() == emails.count()) { + foreach (const QString & author, authors) { + if (!author.isEmpty()) { + aboutData.addAuthor(i18n(author.toUtf8()), QString(), emails[i]); + } + i++; + } + } + showDialog(aboutData); + } else { + const BuiltInEffects::EffectData &data = BuiltInEffects::effectData(builtIn); + KAboutData aboutData(data.name, + data.displayName, + QStringLiteral(KWIN_VERSION_STRING), + data.comment, + KAboutLicense::GPL_V2); + aboutData.setProgramLogo(QIcon::fromTheme(QStringLiteral("preferences-system-windows"))); + aboutData.addAuthor(i18n("KWin development team")); + showDialog(aboutData); + } +} + +void KWinDesktopConfig::slotConfigureEffectClicked() +{ + QString effect; + switch(m_ui->effectComboBox->currentIndex()) { + case 1: + effect = BuiltInEffects::nameForEffect(BuiltInEffect::Slide); + break; + case 2: + effect = BuiltInEffects::nameForEffect(BuiltInEffect::CubeSlide); + break; + default: + return; + } + + QPointer configDialog = new QDialog(this); + KCModule *kcm = KPluginTrader::createInstanceFromQuery(QStringLiteral("kwin/effects/configs/"), QString(), + QStringLiteral("'%1' in [X-KDE-ParentComponents]").arg(effect), + configDialog); + if (!kcm) { + delete configDialog; + return; + } + configDialog->setWindowTitle(m_ui->effectComboBox->currentText()); + configDialog->setLayout(new QVBoxLayout); + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel|QDialogButtonBox::RestoreDefaults, configDialog); + connect(buttons, SIGNAL(accepted()), configDialog, SLOT(accept())); + connect(buttons, SIGNAL(rejected()), configDialog, SLOT(reject())); + connect(buttons->button(QDialogButtonBox::RestoreDefaults), SIGNAL(clicked(bool)), kcm, SLOT(defaults())); + + QWidget *showWidget = new QWidget(configDialog); + QVBoxLayout *layout = new QVBoxLayout; + showWidget->setLayout(layout); + layout->addWidget(kcm); + configDialog->layout()->addWidget(showWidget); + configDialog->layout()->addWidget(buttons); + + if (configDialog->exec() == QDialog::Accepted) { + kcm->save(); + } else { + kcm->load(); + } + delete configDialog; +} + +} // namespace + +#include "main.moc" diff --git a/kcmkwin/kwindesktop/main.h b/kcmkwin/kwindesktop/main.h new file mode 100644 index 0000000..163dc49 --- /dev/null +++ b/kcmkwin/kwindesktop/main.h @@ -0,0 +1,92 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef __MAIN_H__ +#define __MAIN_H__ + +#include +#include + +#include "ui_main.h" + +class KActionCollection; +class KConfigGroup; +class KShortcutsEditor; + +namespace KWin +{ +// if you change this, update also the number of keyboard shortcuts in kwin/kwinbindings.cpp +static const int maxDesktops = 20; +static const int defaultDesktops = 4; + +class KWinDesktopConfigForm : public QWidget, public Ui::KWinDesktopConfigForm +{ + Q_OBJECT + +public: + explicit KWinDesktopConfigForm(QWidget* parent); +}; + +class KWinDesktopConfig : public KCModule +{ + Q_OBJECT + +public: + explicit KWinDesktopConfig(QWidget* parent, const QVariantList& args); + ~KWinDesktopConfig(); + QString cachedDesktopName(int desktop); + + // undo all changes + void undo(); + +public Q_SLOTS: + virtual void save(); + virtual void load(); + virtual void defaults(); + + +private Q_SLOTS: + void slotChangeShortcuts(int number); + void slotShowAllShortcuts(); + void slotEffectSelectionChanged(int index); + void slotAboutEffectClicked(); + void slotConfigureEffectClicked(); + +private: + void init(); + void addAction(const QString &name, const QString &label); + bool effectEnabled(const QString& effect, const KConfigGroup& cfg) const; + QString extrapolatedShortcut(int desktop) const; + +private: + KWinDesktopConfigForm* m_ui; + KSharedConfigPtr m_config; + // cache for desktop names given by NETRootInfo + // needed as the widget only stores the names for actual number of desktops + QStringList m_desktopNames; + // Collection for switching desktops like ctrl+f1 + KActionCollection* m_actionCollection; + // Collection for next, previous, up, down desktop + KActionCollection* m_switchDesktopCollection; + KShortcutsEditor* m_editor; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwindesktop/main.ui b/kcmkwin/kwindesktop/main.ui new file mode 100644 index 0000000..8997fdb --- /dev/null +++ b/kcmkwin/kwindesktop/main.ui @@ -0,0 +1,326 @@ + + + KWinDesktopConfigForm + + + + 0 + 0 + 572 + 310 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + 0 + + + + Desktops + + + + + + Layout + + + + QFormLayout::AllNonFixedFieldsGrow + + + 0 + + + + + Here you can set how many virtual desktops you want on your KDE desktop. + + + &Number of desktops: + + + numberSpinBox + + + + + + + Here you can set how many virtual desktops you want on your KDE desktop. + + + 1 + + + 20 + + + 4 + + + + + + + true + + + N&umber of rows: + + + rowsSpinBox + + + + + + + true + + + 1 + + + 20 + + + + + + + + + + Desktop Names + + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + Switching + + + + + + 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 navigation wraps around + + + + + + + Desktop Effect Animation + + + + QFormLayout::ExpandingFieldsGrow + + + 0 + + + + + Animation: + + + effectComboBox + + + + + + + + + + 0 + 0 + + + + + + + + + 0 + 0 + + + + + + + + + + + + + + + + Desktop Switch On-Screen Display + + + true + + + false + + + + QFormLayout::AllNonFixedFieldsGrow + + + 0 + + + + + Duration: + + + popupHideSpinBox + + + + + + + msec + + + 5000 + + + 50 + + + + + + + Enabling this option will show a small preview of the desktop layout indicating the selected desktop. + + + Show desktop layout indicators + + + + + + + + + + Shortcuts + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + + + + Show shortcuts for all possible desktops + + + + + + + + + + + + + + + KWin::DesktopNamesWidget + QWidget +
desktopnameswidget.h
+ 1 + + numberChanged(int) + +
+
+ + + + numberSpinBox + valueChanged(int) + desktopNames + numberChanged(int) + + + 327 + 144 + + + 326 + 209 + + + + +
diff --git a/kcmkwin/kwinoptions/AUTHORS b/kcmkwin/kwinoptions/AUTHORS new file mode 100644 index 0000000..0615c59 --- /dev/null +++ b/kcmkwin/kwinoptions/AUTHORS @@ -0,0 +1,12 @@ +Please use http://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..ea719b3 --- /dev/null +++ b/kcmkwin/kwinoptions/CMakeLists.txt @@ -0,0 +1,18 @@ +########### next target ############### +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcmkwm\") + +set(kcm_kwinoptions_PART_SRCS windows.cpp mouse.cpp main.cpp ${KWIN_SOURCE_DIR}/effects/effect_builtins.cpp ) +ki18n_wrap_ui(kcm_kwinoptions_PART_SRCS actions.ui advanced.ui focus.ui mouse.ui moving.ui) +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 Qt5::DBus KF5::Completion KF5::I18n KF5::ConfigWidgets KF5::Service KF5::WindowSystem) +install(TARGETS kcm_kwinoptions DESTINATION ${PLUGIN_INSTALL_DIR} ) + + +########### install files ############### + +install( FILES kwinoptions.desktop kwinactions.desktop kwinadvanced.desktop + kwinfocus.desktop kwinmoving.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..928381b --- /dev/null +++ b/kcmkwin/kwinoptions/actions.ui @@ -0,0 +1,660 @@ + + + KWinActionsConfigForm + + + + 0 + 0 + 509 + 309 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + M&ouse wheel: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + coAllW + + + + + + + + 0 + 0 + + + + In this row you can customize middle click behavior when clicking into the titlebar or the frame. + + + + Move + + + + + Activate, Raise & Move + + + + + Toggle Raise & Lower + + + + + Resize + + + + + Raise + + + + + Lower + + + + + Minimize + + + + + Decrease Opacity + + + + + Increase Opacity + + + + + Nothing + + + + + + + + Ri&ght button: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + coAll3 + + + + + + + &Wheel + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + coWinWheel + + + + + + + Middle b&utton: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + coAll2 + + + + + + + + 0 + 0 + + + + In this row you can customize right click behavior when clicking into the titlebar or the frame. + + + + Move + + + + + Activate, Raise & Move + + + + + Toggle Raise & Lower + + + + + Resize + + + + + Raise + + + + + Lower + + + + + Minimize + + + + + Decrease Opacity + + + + + Increase Opacity + + + + + Nothing + + + + + + + + &Left button: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + coWin1 + + + + + + + + 75 + true + + + + Inner Window, Titlebar & Frame + + + + + + + + 0 + 0 + + + + In this row you can customize middle click behavior when clicking into an inactive inner window ('inner' means: not titlebar, not frame). + + + + Activate, Raise & Pass Click + + + + + Activate & Pass Click + + + + + Activate + + + + + Activate & Raise + + + + + + + + &Right button: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + coWin3 + + + + + + + + 0 + 0 + + + + In this row you can customize behavior when scrolling into an inactive inner window ('inner' means: not titlebar, not frame). + + + + Scroll + + + + + Activate & Scroll + + + + + Activate, Raise & Scroll + + + + + + + + + 0 + 0 + + + + In this row you can customize left click behavior when clicking into the titlebar or the frame. + + + + Move + + + + + Activate, Raise & Move + + + + + Toggle Raise & Lower + + + + + Resize + + + + + Raise + + + + + Lower + + + + + Minimize + + + + + Decrease Opacity + + + + + Increase Opacity + + + + + Nothing + + + + + + + + + 0 + 0 + + + + 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 + + + + + Switch to Window Tab to the Left/Right + + + + + Nothing + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + In this row you can customize left click behavior when clicking into an inactive inner window ('inner' means: not titlebar, not frame). + + + + Activate, Raise & Pass Click + + + + + Activate & Pass Click + + + + + Activate + + + + + Activate & Raise + + + + + + + + + 0 + 0 + + + + In this row you can customize right click behavior when clicking into an inactive inner window ('inner' means: not titlebar, not frame). + + + + Activate, Raise & Pass Click + + + + + Activate & Pass Click + + + + + Activate + + + + + Activate & Raise + + + + + + + + M&iddle button: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + coWin2 + + + + + + + + 75 + true + + + + Inactive Inner Window + + + + + + + Left &button + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + coAll1 + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + Modifier &key: + + + coAllKey + + + + + + + Here you select whether holding the Meta key or Alt key will allow you to perform the following actions. + + + + Meta + + + + + Alt + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+
+ + coWin1 + coWin2 + coWin3 + coWinWheel + coAllKey + coAll1 + coAll2 + coAll3 + coAllW + + + +
diff --git a/kcmkwin/kwinoptions/advanced.ui b/kcmkwin/kwinoptions/advanced.ui new file mode 100644 index 0000000..26d8cce --- /dev/null +++ b/kcmkwin/kwinoptions/advanced.ui @@ -0,0 +1,283 @@ + + + KWinAdvancedConfigForm + + + + 0 + 0 + 504 + 387 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Window Tabbing + + + true + + + + + + When turned on hide all tabs that are not active from the taskbar. + + + Hide inactive window tabs from the taskbar + + + + + + + When turned on attempt to automatically detect when a newly opened window is related to an existing one and place them in the same window group. + + + Automatically group similar windows + + + + + + + When turned on immediately switch to any new window tabs that were automatically added to the current group. + + + Switch to automatically grouped windows immediately + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + &Placement: + + + true + + + + + + + 0 + 0 + + + + The placement policy determines where a new window will appear on the desktop.<br><ul> +<li><em>Smart</em> will try to achieve a minimum overlap of windows</li> +<li><em>Maximizing</em> 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><em>Cascade</em> will cascade the windows</li> +<li><em>Random</em> will use a random position</li> +<li><em>Centered</em> will place the window centered</li> +<li><em>Zero-Cornered</em> will place the window in the top-left corner</li> +<li><em>Under Mouse</em> will place the window under the pointer</li> +</ul> + + + + Smart + + + + + Maximizing + + + + + Cascade + + + + + Random + + + + + Centered + + + + + Zero-Cornered + + + + + Under Mouse + + + + + + + + + + + Shading + + + true + + + + + + Dela&y: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + shadeHover + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 24 + 20 + + + + + + + + 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. + + + &Enable hover + + + + + + + Sets the time in milliseconds before the window unshades when the mouse pointer goes over the shaded window. + + + 500 + + + 0 + + + 3000 + + + 100 + + + ms + + + + + + + + + + Special Windows + + + true + + + + + + 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
+
+ + QSpinBox + QWidget +
knuminput.h
+
+
+ + shadeHoverOn + shadeHover + inactiveTabsSkipTaskbar + autogroupSimilarWindows + autogroupInForeground + placementCombo + hideUtilityWindowsForInactive + + + +
diff --git a/kcmkwin/kwinoptions/focus.ui b/kcmkwin/kwinoptions/focus.ui new file mode 100644 index 0000000..5d39c1e --- /dev/null +++ b/kcmkwin/kwinoptions/focus.ui @@ -0,0 +1,585 @@ + + + KWinFocusConfigForm + + + + 0 + 0 + 656 + 540 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Activating windows + + + true + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + 0 + + + + + + + <b>Click To Focus</b><br> +A window becomes active when you click into it.<br><br> +This behaviour is common on other operating systems and<br> +likely what you want. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + false + + + + + + + + + + + <b>Click To Focus - Mouse Precedence</b><br> +This is mostly the same as <i>Click To Focus</i><br><br> +If an active window has to be chosen by the system<br> +(eg. because the currently active one was closed) <br> +the window under the mouse is the preferred candidate.<br><br> +Unusual, but possible variant of <i>Click To Focus</i>. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + false + + + + + + + + + + + <b>Focus Follows Mouse</b><br> +Moving the mouse onto a window will activate it.<br><br> +Eg. windows randomly appearing under the mouse will not gain the focus.<br> +Focus stealing prevention takes place as usual.<br><br> +Think as <i>Click To Focus</i> just without having to actually click. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + false + + + + + + + + + + + <b>Focus Follows Mouse - Mouse Precedence</b><br> +This is mostly the same as <i>Focus Follows Mouse</i><br><br> +If an active window has to be chosen by the system<br> +(eg. because the currently active one was closed) <br> +the window under the mouse is the preferred candidate.<br><br> +Choose this, if you want a hover controlled focus. + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + false + + + + + + + + + + + <b>Focus Under Mouse</b><br> +The focus always remains on the window under the mouse.<br><br> + +Notice:<br> +<b>Focus stealing prevention</b> and the <b>tabbox ("Alt+Tab")</b><br> +contradict the policy and <b>will not work</b>.<br><br> +You very likely want to use<br> +<i>Focus Follows Mouse - Mouse Precedence</i> instead! + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + false + + + + + + + + + + + <b>Focus Strictly Under Mouse</b><br> +The focus is always on the window under the mouse - in doubt nowhere -<br> +very much like the focus behaviour in an unmanaged legacy X11 environment.<br><br> + +Notice:<br> +<b>Focus stealing prevention</b> and the <b>tabbox ("Alt+Tab")</b><br> +contradict the policy and <b>will not work</b>.<br><br> +You very likely want to use<br> +<i>Focus Follows Mouse - Mouse Precedence</i> instead! + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + + + false + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Delay focus by + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + delayFocus + + + + + + + + 0 + 0 + + + + This is the delay after which the window the mouse pointer is over will automatically receive focus. + + + ms + + + 0 + + + 3000 + + + 100 + + + 99 + + + + + + + + + + + Focus &stealing prevention + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + focusStealing + + + + + + + + 0 + 0 + + + + <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 Focus Under Mouse or Focus Strictly Under Mouse focus policies.) +<ul> +<li><em>None:</em> Prevention is turned off and new windows always become activated.</li> +<li><em>Low:</em> 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><em>Medium:</em> Prevention is enabled.</li> +<li><em>High:</em> 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><em>Extreme:</em> All windows must be explicitly activated by the user.</li> +</ul></p> +<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> + + + + None + + + + + Low + + + + + Medium + + + + + High + + + + + Extreme + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 0 + + + + + + 0 + 0 + + + + + 75 + true + + + + Policy + + + + + + + + 0 + 0 + + + + Click + + + + + + + 5 + + + 1 + + + Qt::Horizontal + + + QSlider::NoTicks + + + 0 + + + + + + + + 0 + 0 + + + + Hover + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Raising windows + + + true + + + + + + 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 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + This is the delay after which the window that the mouse pointer is over will automatically come to the front. + + + ms + + + 0 + + + 3000 + + + 100 + + + 99 + + + + + + + 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. + + + C&lick raises active window + + + + + + + + + + Qt::Vertical + + + + 20 + 192 + + + + + + + + Multiscreen behaviour + + + true + + + + + + When this option is enabled, focus operations are limited only to the active Xinerama screen + + + S&eparate screen focus + + + + + + + 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 + + + + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+
+ + focusStealing + autoRaiseOn + autoRaise + delayFocus + separateScreenFocus + activeMouseScreen + + + + + windowFocusPolicy + valueChanged(int) + stackedWidget + setCurrentIndex(int) + + + 244 + 38 + + + 444 + 84 + + + + +
diff --git a/kcmkwin/kwinoptions/kwinactions.desktop b/kcmkwin/kwinoptions/kwinactions.desktop new file mode 100644 index 0000000..dfa2be1 --- /dev/null +++ b/kcmkwin/kwinoptions/kwinactions.desktop @@ -0,0 +1,188 @@ +[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=Actions +Name[af]=Aksies +Name[ar]=إجراءات +Name[be]=Дзеянні +Name[be@latin]=Aperacyi +Name[bg]=Действия +Name[bn]=কাজ +Name[bn_IN]=কর্ম +Name[br]=Oberoù +Name[bs]=Radnje +Name[ca]=Accions +Name[ca@valencia]=Accions +Name[cs]=Činnosti +Name[csb]=Dzejania +Name[cy]=Gweithredoedd +Name[da]=Handlinger +Name[de]=Aktionen +Name[el]=Ενέργειες +Name[en_GB]=Actions +Name[eo]=Agoj +Name[es]=Acciones +Name[et]=Tegevused +Name[eu]=Ekintzak +Name[fa]=کنشها +Name[fi]=Toiminnot +Name[fr]=Actions +Name[fy]=Aksjes +Name[ga]=Gníomhartha +Name[gl]=Accións +Name[gu]=ક્રિયાઓ +Name[he]=פעולות +Name[hi]=क्रियाएं +Name[hne]=काम +Name[hr]=Aktivnosti +Name[hu]=Műveletek +Name[ia]=Actiones +Name[id]=Actions +Name[is]=Aðgerðir +Name[it]=Azioni +Name[ja]=動作 +Name[ka]=ქცევა +Name[kk]=Амалдар +Name[km]=អំពើ +Name[kn]=ಕ್ರಿಯೆಗಳು +Name[ko]=동작 +Name[ku]=Çalakî +Name[lt]=Veiksmai +Name[lv]=Darbības +Name[mai]=क्रियासभ +Name[mk]=Акции +Name[ml]=പ്രവര്‍ത്തനങ്ങള്‍ +Name[mr]=क्रिया +Name[ms]=Tindakan +Name[nb]=Handlinger +Name[nds]=Akschonen +Name[ne]=कार्य +Name[nl]=Acties +Name[nn]=Handlingar +Name[oc]=Accions +Name[pa]=ਕਾਰਵਾਈਆਂ +Name[pl]=Działania +Name[pt]=Acções +Name[pt_BR]=Ações +Name[ro]=Acțiuni +Name[ru]=Действия +Name[se]=Doaimmat +Name[si]=ක්‍රියා +Name[sk]=Akcie +Name[sl]=Dejanja +Name[sr]=Радње +Name[sr@ijekavian]=Радње +Name[sr@ijekavianlatin]=Radnje +Name[sr@latin]=Radnje +Name[sv]=Åtgärder +Name[ta]=செயல்கள் +Name[te]=చర్యలు +Name[tg]=Амалҳо +Name[th]=การกระทำ +Name[tr]=Eylemler +Name[ug]=مەشغۇلاتلار +Name[uk]=Дії +Name[uz]=Amallar +Name[uz@cyrillic]=Амаллар +Name[vi]=Hành động +Name[wa]=Accions +Name[xh]=Iintshukumo +Name[x-test]=xxActionsxx +Name[zh_CN]=操作 +Name[zh_TW]=動作 + +Comment=Mouse Actions on Windows +Comment[bs]=Akcioje miša na prozorima +Comment[ca]=Accions del ratolí en les finestres +Comment[ca@valencia]=Accions del ratolí en les finestres +Comment[cs]=Činnosti myši na oknech +Comment[da]=Musehandlinger på vinduer +Comment[de]=Maus-Aktionen für Fenster +Comment[el]=Ενέργειες ποντικιού στα παράθυρα +Comment[en_GB]=Mouse Actions on Windows +Comment[es]=Acciones del ratón sobre las ventanas +Comment[et]=Hiiretoimingud akendes +Comment[eu]=Sagu-ekintzak leihoetan +Comment[fi]=Ikkunoiden hiiritoiminnot +Comment[fr]=Actions de souris sur les fenêtres +Comment[gl]=Accións do rato nas xanelas +Comment[he]=הגדרות פעולות עכבר +Comment[hu]=Egérműveletek az ablakokon +Comment[ia]=Actiones de mus sur fenestras +Comment[id]=Aksi Mouse di Jendela +Comment[it]=Azioni del mouse sulle finestre +Comment[ja]=ウインドウ上でのマウスアクション +Comment[ko]=창 마우스 동작 설정 +Comment[lt]=Pelės veiksmai ant langų +Comment[nb]=Musehandlinger på vinduer +Comment[nds]=Muusakschonen för Finstern fastleggen +Comment[nl]=Muisacties op vensters +Comment[nn]=Musehandlingar på vindauge +Comment[pa]=ਵਿੰਡੋਜ਼ ਉੱਤੇ ਮਾਊਸ ਐਕਸ਼ਨ +Comment[pl]=Działania myszy na oknach +Comment[pt]=Acções do Rato nas Janelas +Comment[pt_BR]=Ações do mouse nas janelas +Comment[ru]=Настройка действий мыши для окон +Comment[sk]=Akcie myši na oknách +Comment[sl]=Dejanja miške na oknih +Comment[sr]=Радње мишем над прозорима +Comment[sr@ijekavian]=Радње мишем над прозорима +Comment[sr@ijekavianlatin]=Radnje mišem nad prozorima +Comment[sr@latin]=Radnje mišem nad prozorima +Comment[sv]=Musåtgärder för fönster +Comment[tr]=Pencerelerde Fare Eylemleri +Comment[uk]=Дії над вікнами за допомогою миші +Comment[x-test]=xxMouse Actions on Windowsxx +Comment[zh_CN]=窗口的鼠标动作 +Comment[zh_TW]=視窗上的滑鼠動作 + +X-KDE-Keywords=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize +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,bilah judul,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[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[ru]=shade,maximise,maximize,minimize,minimise,lower,operations menu,titlebar,resize,свернуть,распахнуть,убрать вниз,меню операций,меню действий,заголовок окна,заголовок,изменить размер +X-KDE-Keywords[sk]=tieň,maximalizácia,maximalizovanie,minimalizácia,minimalizovanie,nižsí,ponuka operácií,titulkový pruh,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..514ca02 --- /dev/null +++ b/kcmkwin/kwinoptions/kwinadvanced.desktop @@ -0,0 +1,186 @@ +[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 +Name[af]=Gevorderd +Name[ar]=متقدم +Name[be]=Асаблівы +Name[be@latin]=Asablivaje +Name[bg]=Допълнителни +Name[bn]=অগ্রসর +Name[bn_IN]=উন্নত বৈশিষ্ট্য +Name[br]=Barek +Name[bs]=Napredno +Name[ca]=Avançat +Name[ca@valencia]=Avançat +Name[cs]=Pokročilé +Name[csb]=Awansowóné +Name[cy]=Uwch +Name[da]=Avanceret +Name[de]=Erweitert +Name[el]=Για προχωρημένους +Name[en_GB]=Advanced +Name[eo]=Pliaj +Name[es]=Avanzado +Name[et]=Muu +Name[eu]=Aurreratua +Name[fa]=پیشرفته +Name[fi]=Lisäasetukset +Name[fr]=Avancé +Name[fy]=Avansearre +Name[ga]=Casta +Name[gl]=Avanzado +Name[gu]=ઉચ્ચ +Name[he]=הגדרות מתקדמות +Name[hi]=विस्तृत +Name[hne]=विस्तृत +Name[hr]=Napredno +Name[hu]=Speciális +Name[ia]=Avantiate +Name[id]=Advanced +Name[is]=Ítarlegt +Name[it]=Avanzate +Name[ja]=詳細 +Name[ka]=დამატებით +Name[kk]=Жетелеу +Name[km]=កម្រិត​ខ្ពស់ +Name[kn]=ಪ್ರೌಢ +Name[ko]=고급 +Name[ku]=Pêşketî +Name[lt]=Sudėtingesni +Name[lv]=Paplašināti +Name[mai]=उन्नत +Name[mk]=Напредни +Name[ml]=സങ്കീര്‍ണ്ണമായ +Name[mr]=प्रगत +Name[ms]=Lanjutan +Name[nb]=Avansert +Name[nds]=Verwiedert +Name[ne]=उन्नत +Name[nl]=Geavanceerd +Name[nn]=Avansert +Name[oc]=A_vançat +Name[pa]=ਤਕਨੀਕੀ +Name[pl]=Zaawansowane +Name[pt]=Avançado +Name[pt_BR]=Avançado +Name[ro]=Avansat +Name[ru]=Дополнительно +Name[se]=Viiddiduvvon +Name[si]=උසස් +Name[sk]=Pokročilé +Name[sl]=Napredno +Name[sr]=Напредно +Name[sr@ijekavian]=Напредно +Name[sr@ijekavianlatin]=Napredno +Name[sr@latin]=Napredno +Name[sv]=Avancerat +Name[ta]=உயர்நிலை +Name[te]=ఆధునాతన +Name[tg]=Иловагӣ +Name[th]=ขั้นสูง +Name[tr]=Gelişmiş +Name[ug]=ئالىي +Name[uk]=Додатково +Name[uz]=Qoʻshimcha +Name[uz@cyrillic]=Қўшимча +Name[vi]=Nâng cao +Name[wa]=Sipepieus +Name[xh]=Ebhekisa phambili +Name[x-test]=xxAdvancedxx +Name[zh_CN]=高级 +Name[zh_TW]=進階 + +Comment=Advanced Window Management Features +Comment[bs]=Napredne mogućnosti upravljanja prozoeima +Comment[ca]=Característiques avançades per a la gestió de les finestres +Comment[ca@valencia]=Característiques avançades per a la gestió de les finestres +Comment[cs]=Pokročilé vlastností správy oken +Comment[da]=Avancerede vindueshåndteringsegenskaber +Comment[de]=Erweiterte Fensterverwaltung +Comment[el]=Διαμόρφωση προχωρημένων χαρακτηριστικών της διαχείρισης παραθύρων +Comment[en_GB]=Advanced Window Management Features +Comment[es]=Funciones avanzadas del gestor de ventanas +Comment[et]=Muud aknahalduse omadused +Comment[eu]=Leiho kudeaketaren ezaugarri aurreratuak +Comment[fi]=Ikkunoinnin lisäominaisuudet +Comment[fr]=Fonctionnalités de gestion avancée des fenêtres +Comment[gl]=Funcionalidades avanzadas da xestión de xanelas +Comment[he]=תכונות ניהול חלונות מתקדמים +Comment[hu]=Speciális ablakkezelési szolgáltatások +Comment[ia]=Characteristicas avantiate de gestion de fenestra +Comment[id]=Fitur Pengelolaan Jendela Tingkatlanjut +Comment[it]=Funzionalità avanzate della gestione delle finestre +Comment[ja]=高度なウインドウ管理機能 +Comment[ko]=고급 창 관리자 기능 설정 +Comment[lt]=Išsamesnės langų tvarkymo savybės +Comment[nb]=Funksjoner for avansert vindusbehandling +Comment[nds]=Verwiedert Finsterinstellen +Comment[nl]=Geavanceerde vensterbeheermogelijkheden +Comment[nn]=Avanserte vindaugshandsamarfunksjonar +Comment[pa]=ਤਕਨੀਕੀ ਵਿੰਡੋ ਮੈਨਜੇਮੈਂਟ ਫੀਚਰ +Comment[pl]=Zaawansowane ustawienia zarządzania oknami +Comment[pt]=Funcionalidades de Gestão de Janelas Avançadas +Comment[pt_BR]=Recursos avançados de gerenciamento de janelas +Comment[ru]=Настройка дополнительных возможностей управления окнами +Comment[sk]=Pokročilé možnosti správy okien +Comment[sl]=Napredne zmožnosti upravljanja oken +Comment[sr]=Напредне могућности управљања прозорима +Comment[sr@ijekavian]=Напредне могућности управљања прозорима +Comment[sr@ijekavianlatin]=Napredne mogućnosti upravljanja prozorima +Comment[sr@latin]=Napredne mogućnosti upravljanja prozorima +Comment[sv]=Avancerade fönsterhanteringsfunktioner +Comment[tr]=Gelişmiş Pencere Yönetim Özellikleri +Comment[uk]=Додаткові можливості з керування вікнами +Comment[x-test]=xxAdvanced Window Management Featuresxx +Comment[zh_CN]=高级窗口管理特性 +Comment[zh_TW]=進階視窗管理功能 + +X-KDE-Keywords=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,window placement,placement of windows,window advanced behavior +X-KDE-Keywords[bs]=sjenčanje, granične, lebdjenje, aktivne granice, popločavanje, Kartice, tabovanje, prozorno tabovanje, grupiranje prozora, pločica, prozorna pločica, plasman przora, plasman prozorâ, napredo ponašanje prozora +X-KDE-Keywords[ca]=ombra,vora,passar per sobre,vores actives,mosaic,pestanyes,pestanyes de finestra,agrupació de les finestres,mosaic de les finestres,col·locació de les finestres,comportament avançat de les finestres +X-KDE-Keywords[ca@valencia]=ombra,vora,passar per sobre,vores actives,mosaic,pestanyes,pestanyes de finestra,agrupació de les finestres,mosaic de les finestres,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 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[el]=σκίαση,περίγραμμα,αιώρηση,ενεργά περιγράμματα,παράθεση,στηλοθέτες,στηλοθέτηση,στηλοθέτηση παραθύρων,ομαδοποίηση παραθύρων,παράθεση παραθύρων,τοποθέτηση παραθύρων,τοποθέτηση παραθύρων,προχωρημένη συμπεριφορά παραθύρων +X-KDE-Keywords[en_GB]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,window placement,placement of windows,window advanced behaviour +X-KDE-Keywords[es]=sombra,borde,pasada,bordes activos,mosaico,pestañas,páginas en pestañas,pestañas de páginas,agrupación de ventanas,ventanas en mosaico,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 +X-KDE-Keywords[eu]=biltzea,ertza,gainetik pasatzea,ertz aktiboak,lauza, fitxak, leihoen fitxak,leihoak lauza moduan,leihoen kokalekua,leihoen portaera aurreratua +X-KDE-Keywords[fi]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,window placement,placement of windows,window advanced behavior,varjostus,raja,kohdistus,aktiiviset reunat,välilehdet,ikkunoiden ryhmittely,ikkunoiden sijoittelu,ikkunoiden lisäasetukset +X-KDE-Keywords[fr]=ombres, bord, survol, bords actifs, mosaïque, onglets, tabulation, changement d'onglet, groupement de fenêtres, mosaïque de fenêtres, placement de fenêtres, comportement avancé des fenêtres +X-KDE-Keywords[gl]=sombra,bordo,beira,pasar,bordos activos,beiras activas,lapelas,agrupar xanelas, situación das xanelas, posicionamento das xanelas,comportamento avanzado das xanelas +X-KDE-Keywords[hu]=árnyékolás,szegély,lebegés,aktív szegélyek,csempézés,bejárás,ablakbejárás,ablakcsoportosítás,ablakcsempézés,ablakelhelyezés,ablakok elhelyezése,ablak speciális viselkedése +X-KDE-Keywords[ia]=umbrar,margine,planante,margines active,con tegulas,schedas,tabbing,tabbing de fenestra,gruppante fenestra,fenestra con tegulas,placiamento de fenestra,placiamento de fenestras, comportamento avantiate de fenestra +X-KDE-Keywords[id]=bayangan,batas,melayang,batas aktif,ubin,tab,tab,tab jendela,grup jendela,ubin jendela,penempatan jendela,penempatan jendela,perilaku lanjutan jendela +X-KDE-Keywords[it]=ombreggiatura,bordo,sovrapponi,bordi attivi,affiancamento,schede,navigazione schede,finestre a schede,raggruppamento finestre,affiancamento finestre,posizionamento finestre,posizionamento delle finestre,comportamento avanzato delle finestre +X-KDE-Keywords[kk]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,window placement,placement of windows,window advanced behavior +X-KDE-Keywords[km]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,window placement,placement of windows,window advanced behavior +X-KDE-Keywords[ko]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,window placement,placement of windows,window advanced behavior,그림자,경계선,호버,지나다니기,타일,탭,창 탭,창 그룹,창 타일,창 위치 +X-KDE-Keywords[nb]=-gardinrulling,kant,sveve,aktive kanter,flislegging,faner,vindusfaner,vindusgruppering,vindus-flislegging,vindusplassering,plassering av vinduer,avansert vindusoppførsel +X-KDE-Keywords[nds]=Inrullen,Rahmen,sweven,aktive Kanten,kacheln,Paneels,wesseln,Finster,Finsterkoppel,utrichten,verwiedert,Platzeren +X-KDE-Keywords[nl]=verduisteren,rand,overzweven,actieve randen,schuin achter elkaar,tabbladen,met tabbladen werken,vensterwisseling,verstergroepering,vensters schuin achter elkaar,vensterplaatsing,plaatsing van vensters,geavanceerd gedrag van vensters +X-KDE-Keywords[nn]=opprulling,kant,sveva,aktive kantar,flislegging,faner,vindaugsfaner,vindaugsgruppering,vindaugsflislegging,vindaugsplassering,plassering av vindauge,avansert vindaugsåtferd +X-KDE-Keywords[pl]=zwijanie,obramowanie,unoszenie,aktywne obramowania,kafelkowanie,karty,tworzenie kart, umieszczanie okien w kartach,grupowanie okien,kafelkowanie okien,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]=sombra,contorno,passagem,contornos ativos,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[ru]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,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]=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[sr]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,window placement,placement of windows,window advanced behavior,сенка,ивица,лебдење,активне ивице,поплочавање,језичци,прозори под језичцима,груписање прозора,поплочавање прозора,постављење прозора,напредног понашање прозора +X-KDE-Keywords[sr@ijekavian]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,window placement,placement of windows,window advanced behavior,сенка,ивица,лебдење,активне ивице,поплочавање,језичци,прозори под језичцима,груписање прозора,поплочавање прозора,постављење прозора,напредног понашање прозора +X-KDE-Keywords[sr@ijekavianlatin]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,window placement,placement of windows,window advanced behavior,senka,ivica,lebdenje,aktivne ivice,popločavanje,jezičci,prozori pod jezičcima,grupisanje prozora,popločavanje prozora,postavljenje prozora,naprednog ponašanje prozora +X-KDE-Keywords[sr@latin]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,window placement,placement of windows,window advanced behavior,senka,ivica,lebdenje,aktivne ivice,popločavanje,jezičci,prozori pod jezičcima,grupisanje prozora,popločavanje prozora,postavljenje prozora,naprednog ponašanje prozora +X-KDE-Keywords[sv]=skuggning,kanter,hålla musen över,aktiva kanter,sida vid sida,flikar,fönsterflikar,fönstergruppering,fönster sida vid sida,fönsterplacering,placering av fönster,avancerat fönsterbeteende +X-KDE-Keywords[tr]=gölgeleme,geri yükleme,kenarlık,üzerine gelme,etkin kenarlık,döşeme,sekmeler,sekmeleme,pencere sekmeleme,pencere gruplama,pencere döşeme,pencere konumlandırma,pencere yerleşimi,gelişmiş pencere davranışları +X-KDE-Keywords[uk]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,window placement,placement of windows,window advanced behavior,тіні,границі,межі,краї,активні краї,плитка,тайлінґ,вкладки,мозаїка,вікно з вкладками,групування вікон,розташування вікон, додаткові ефекти поведінки +X-KDE-Keywords[x-test]=xxshadingxx,xxborderxx,xxhoverxx,xxactive bordersxx,xxtilingxx,xxtabsxx,xxtabbingxx,xxwindow tabbingxx,xxwindow groupingxx,xxwindow tilingxx,xxwindow placementxx,xxplacement of windowsxx,xxwindow advanced behaviorxx +X-KDE-Keywords[zh_CN]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,window placement,placement of windows,window advanced behavior,阴影,边框,悬停,激活边界,平铺,标签,窗口标签,窗口分组,平铺窗口,窗口位置,窗口高级行为 +X-KDE-Keywords[zh_TW]=shading,border,hover,active borders,tiling,tabs,tabbing,window tabbing,window grouping,window tiling,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..3e9f635 --- /dev/null +++ b/kcmkwin/kwinoptions/kwinfocus.desktop @@ -0,0 +1,182 @@ +[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=Focus +Name[af]=Fokus +Name[ar]=التركيز +Name[be]=Фокус +Name[be@latin]=Fokus +Name[bg]=Фокус +Name[bn]=ফোকাস +Name[br]=Fokus +Name[bs]=Fokus +Name[ca]=Focus +Name[ca@valencia]=Focus +Name[cs]=Zaměření +Name[csb]=Zrëszanié +Name[cy]=Canolbwynt +Name[da]=Fokus +Name[de]=Aktivierung +Name[el]=Εστίαση +Name[en_GB]=Focus +Name[eo]=Fokuso +Name[es]=Foco +Name[et]=Fookus +Name[eu]=Fokua +Name[fa]=کانون +Name[fi]=Kohdistus +Name[fr]=Focus +Name[fy]=Focus +Name[ga]=Fócas +Name[gl]=Foco +Name[gu]=ધ્યાન +Name[he]=התמקדות +Name[hi]=फ़ोकस +Name[hne]=फोकस +Name[hr]=Fokus +Name[hu]=Fókuszálás +Name[ia]=Foco +Name[id]=Focus +Name[is]=Virkni +Name[it]=Attivazione +Name[ja]=フォーカス +Name[ka]=ფოკუსი +Name[kk]=Назар +Name[km]=ផ្ដោត +Name[kn]=ನಾಭೀಕರಿಸು (ಫೋಕಸ್) +Name[ko]=초점 +Name[ku]=Nîvend Bike +Name[lt]=Fokusas +Name[lv]=Fokuss +Name[mai]=फोकस +Name[mk]=Фокусирање +Name[ml]=ഫോക്കസ് +Name[mr]=केंद्र +Name[ms]=Fokus +Name[nb]=Fokus +Name[nds]=Fokus +Name[ne]=फोकस +Name[nl]=Focus +Name[nn]=Fokus +Name[pa]=ਫੋਕਸ +Name[pl]=Uaktywnianie +Name[pt]=Foco +Name[pt_BR]=Foco +Name[ro]=Focalizare +Name[ru]=Фокус +Name[se]=Fohkus +Name[si]=නාඹිගත කරන්න +Name[sk]=Zameranie +Name[sl]=Žarišče +Name[sr]=Фокус +Name[sr@ijekavian]=Фокус +Name[sr@ijekavianlatin]=Fokus +Name[sr@latin]=Fokus +Name[sv]=Fokus +Name[ta]=முனைப்படுத்து +Name[te]=దృష్టి +Name[tg]=Фокус +Name[th]=การโฟกัส +Name[tr]=Odaklama +Name[ug]=فوكۇس +Name[uk]=Фокус +Name[uz]=Fokus +Name[uz@cyrillic]=Фокус +Name[vi]=Tập trung +Name[wa]=Focus +Name[xh]=Focus +Name[x-test]=xxFocusxx +Name[zh_CN]=对焦 +Name[zh_TW]=焦點 + +Comment=Active Window Policy +Comment[bs]=Pravila aktivnih prozora +Comment[ca]=Política de la finestra activa +Comment[ca@valencia]=Política de la finestra activa +Comment[cs]=Chování aktivního okna +Comment[da]=Politik for aktivt vindue +Comment[de]=Richtlinie für aktives Fenster +Comment[el]=Πολιτική ενεργού παραθύρου +Comment[en_GB]=Active Window Policy +Comment[es]=Política de la ventana activa +Comment[et]=Aktiivse akna reegel +Comment[eu]=Leiho aktiboentzako politika +Comment[fi]=Aktiivisen ikkunan valintatapa +Comment[fr]=Politique pour les fenêtres actives +Comment[gl]=Política da xanela activa +Comment[he]=מדיניות חלון פעיל +Comment[hu]=Aktív ablakok szabályai +Comment[id]=Kebijakan Jendela Aktif +Comment[it]=Politica finestra attiva +Comment[ko]=활성 창 정책 +Comment[lt]=Aktyvus lango taisyklės +Comment[nb]=ActiveWindow-styring +Comment[nds]=Regel för't aktive Finster +Comment[nl]=Beleid voor actief venster +Comment[nn]=Aktiv vindaugsstyring +Comment[pa]=ਸਰਗਰਮ ਵਿੰਡੋ ਪਾਲਸੀ +Comment[pl]=Zasady aktywowania okna +Comment[pt]=Política da Janela Activa +Comment[pt_BR]=Política da janela ativa +Comment[ru]=Правила смены активного окна +Comment[sk]=Politika aktívneho okna +Comment[sl]=Pravilnik dejavnih oken +Comment[sr]=Смерница активирања прозора +Comment[sr@ijekavian]=Смерница активирања прозора +Comment[sr@ijekavianlatin]=Smernica aktiviranja prozora +Comment[sr@latin]=Smernica aktiviranja prozora +Comment[sv]=Aktiv fönsterprincip +Comment[tr]=Etkin Pencere Politikası +Comment[uk]=Правила для задіяння вікон +Comment[x-test]=xxActive Window Policyxx +Comment[zh_CN]=活动窗口策略 +Comment[zh_TW]=作用中視窗政策 + +X-KDE-Keywords=focus,placement,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[bs]=fokus, smještaj, automatski rast, rast, kliknite na rast, tastatura, CDE, Alt-Tab, cijeli desktop, fokus slijedi miša, fokus prevenciju, usredotoči krade, fokus politike, fokus prozora ponašanje, prozor zaslon ponašanje +X-KDE-Keywords[ca]=focus,col·locació,elevació automàtica,elevació,elevació en clic,teclat,CDE,alt-tab,tots els escriptoris,focus segueix el ratolí,prevenció de focus,robatori de focus,política de focus,comportament del focus de la finestra,comportament en pantalla de la finestra +X-KDE-Keywords[ca@valencia]=focus,col·locació,elevació automàtica,elevació,elevació en clic,teclat,CDE,alt-tab,tots els escriptoris,focus segueix el ratolí,prevenció de focus,robatori de focus,política de focus,comportament del focus de la finestra,comportament en pantalla de la finestra +X-KDE-Keywords[da]=fokus,placering,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,Anordnung,Platzierung,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[el]=εστίαση,τοποθέτηση,αυτόματη αύξηση,αύξηση,αύξηση κλικ,πληκτρολόγιο,CDE,alt-tab,όλες οι επιφάνειες εργασίας,εστίαση ακολουθεί ποντίκι,πρόληψη εστίασης,κλοπή εστίασης,πολιτική εστίασης,συμπεριφορά εστίασης παραθύρων,συμπεριφορά οθόνης παραθύρων +X-KDE-Keywords[en_GB]=focus,placement,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,posicionamiento,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,paigutus,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,kokaleku,automatikoki igo,igo,egin klik igotzeko,teklatu,DE,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,focus,placement,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[fr]=focus, placement, 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,posicionamento,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[hu]=fókusz,elhelyezés,automatikus felemelés,felemelés,kattintásra felemelés,billentyűzet,CDE,alt-tab,összes asztal,egérkövető fókusz,fókuszmegelőzés,fókuszlopás,fókusz házirend,ablakfókusz működése,ablakképernyő működése +X-KDE-Keywords[ia]=focus,placiamento,auto raise,raise,click raise,clavierp,CDE,alt-tab,all desktop,focus seque mus,prevention de focus,focus stealing,politica de focus,comportamento de foco de fenestra,comportamento de schermo de fenestra +X-KDE-Keywords[id]=fokus,penempatan,naikkan otomatis,naikkan,klik naikkan,papan ketik,CDE,alt-tab,semua desktop,fokus mengikuti mouse,pencegahan fokus,pencurian fokus,kebijakan fokus,perilaku fokus jendela,perilaku layar jendela +X-KDE-Keywords[it]=fuoco,posizionamento,avanzamento automatico,avanzamento,avanzamento con clic,tastiera,CDE,alt-tab,tutti i desktop,il fuco segue il mouse,impedisci il fuoco,mantieni il fuoco,regole fuoco,regole fuoco finestra,comportamento finestra +X-KDE-Keywords[kk]=focus,placement,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[km]=focus,placement,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[ko]=focus,placement,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[nb]=fokus,plassering,autohev,hev,klikk-hev,tastatur,CDE,alt-tab,alle skrivebord,fokus følger mus,fokushindring,fokus-stjeling,fokuspraksis,fokusoppførsel for vinduer,vindusoppførsel på skjerm +X-KDE-Keywords[nds]=Fokus,Platzeren,automaatsch,na vörn,op Klick,Tastatuur,CDE,Alt-Tab,all Schriefdischen,Muusfokus,verhöden,verleren,Fokusregel,Schirm,bedregen +X-KDE-Keywords[nl]=focus,plaatsing,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,plassering,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,umieszczenie,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,colocação,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,placement,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škipreprečevanje fokusa,preprečevanje žarišča,kraja fokusa,kraja žarišča,pravila fokusiranja,pravila za žarišče,obnašanje pri fokusiranju oken,obnašanje pri postavljanju oken v žarišče,obnašanje oken +X-KDE-Keywords[sr]=focus,placement,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,фокус,постављење,аутоматско дизање,дизање,дизање кликом,тастатура,ЦДЕ,Alt-Tab,све површи,фокус прати миш,спречавање фокуса,крађа фокуса,смерница фокуса,понашање фокусирања прозора +X-KDE-Keywords[sr@ijekavian]=focus,placement,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,фокус,постављење,аутоматско дизање,дизање,дизање кликом,тастатура,ЦДЕ,Alt-Tab,све површи,фокус прати миш,спречавање фокуса,крађа фокуса,смерница фокуса,понашање фокусирања прозора +X-KDE-Keywords[sr@ijekavianlatin]=focus,placement,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,fokus,postavljenje,automatsko dizanje,dizanje,dizanje klikom,tastatura,CDE,Alt-Tab,sve površi,fokus prati miš,sprečavanje fokusa,krađa fokusa,smernica fokusa,ponašanje fokusiranja prozora +X-KDE-Keywords[sr@latin]=focus,placement,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,fokus,postavljenje,automatsko dizanje,dizanje,dizanje klikom,tastatura,CDE,Alt-Tab,sve površi,fokus prati miš,sprečavanje fokusa,krađa fokusa,smernica fokusa,ponašanje fokusiranja prozora +X-KDE-Keywords[sv]=fokus,placering,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[tr]=odakla,odak,yerleşim,otomatik yükselt,yükselt,tıkla yükselt,klavye,CDE,alt-tab,tüm masaüstleri,odak fareyi takip etsin,odaklama engelleme,odak çalma,odaklama politikası,pencere odaklama davranışı,pencere ekran davranışı +X-KDE-Keywords[uk]=focus,placement,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[x-test]=xxfocusxx,xxplacementxx,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,placement,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,placement,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..066e811 --- /dev/null +++ b/kcmkwin/kwinoptions/kwinmoving.desktop @@ -0,0 +1,185 @@ +[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=Moving +Name[af]=Beweeg +Name[ar]=التحريك +Name[be]=Перамяшчэнне +Name[be@latin]=Pierasoŭvańnie +Name[bg]=Преместване +Name[bn]=সরানো হচ্ছে +Name[br]=O tilec'hiañ +Name[bs]=Pomeranje +Name[ca]=Moviment +Name[ca@valencia]=Moviment +Name[cs]=Přesouvání +Name[csb]=Przesëwanié +Name[cy]=Symud +Name[da]=Flytter +Name[de]=Verschieben +Name[el]=Μετακίνηση +Name[en_GB]=Moving +Name[eo]=Movanta +Name[es]=Moviendo +Name[et]=Liigutamine +Name[eu]=Mugitzea +Name[fa]=حرکت +Name[fi]=Siirtäminen +Name[fr]=Déplacement +Name[fy]=Ferpleatsing +Name[ga]=Bogadh +Name[gl]=Movemento +Name[gu]=ખસેડવું +Name[he]=הזזה +Name[hi]=खिसकाते हुए +Name[hne]=खिसकावत हे +Name[hr]=Pomicanje +Name[hu]=Mozgatás +Name[ia]=Movente +Name[id]=Moving +Name[is]=Færa +Name[it]=Spostamento +Name[ja]=移動 +Name[ka]=გადაადგილება +Name[kk]=Жылжыту +Name[km]=ការ​ផ្លាស់ទី +Name[kn]=ಸರಿಸುವಿಕೆ +Name[ko]=이동 +Name[ku]=Guhestin +Name[lt]=Perkėlimas +Name[lv]=Pārvietošana +Name[mai]=पठाए रहल +Name[mk]=Движење +Name[ml]=നീക്കുന്നു +Name[mr]=हलवित आहे +Name[nb]=Flytting +Name[nds]=Verschuven +Name[ne]=सार्दा +Name[nl]=Verplaatsing +Name[nn]=Flytting +Name[oc]=Desplaçament +Name[pa]=ਏਧਰ ਓਧਰ ਕਰੋ +Name[pl]=Przesuwanie +Name[pt]=Mover +Name[pt_BR]=Movendo +Name[ro]=Mutare +Name[ru]=Перемещение +Name[se]=Lihkadeamen +Name[si]=ගෙනයමින් +Name[sk]=Presun +Name[sl]=Premikanje +Name[sr]=Померање +Name[sr@ijekavian]=Помијерање +Name[sr@ijekavianlatin]=Pomijeranje +Name[sr@latin]=Pomeranje +Name[sv]=Förflyttning +Name[ta]=நகர்கிறது +Name[te]=కదుపు +Name[tg]=Таҳвилкунӣ +Name[th]=การย้าย +Name[tr]=Taşıma +Name[ug]=يۆتكەۋاتىدۇ +Name[uk]=Пересування +Name[uz]=Koʻchirish +Name[uz@cyrillic]=Кўчириш +Name[vi]=Di chuyển +Name[wa]=Bodjî +Name[xh]=Iyahamba +Name[x-test]=xxMovingxx +Name[zh_CN]=移动中 +Name[zh_TW]=移動 + +Comment=Window Moving +Comment[bs]=Kretanje prozora +Comment[ca]=Moviment de les finestres +Comment[ca@valencia]=Moviment de les finestres +Comment[cs]=Posun oken +Comment[da]=Flytning af vinduer +Comment[de]=Fensterbewegung +Comment[el]=Μετακίνηση παραθύρου +Comment[en_GB]=Window Moving +Comment[es]=Movimiento de ventanas +Comment[et]=Akna liigutamine +Comment[eu]=Leihoak mugitzea +Comment[fi]=Ikkunoiden siirtäminen +Comment[fr]=Déplacement des fenêtres +Comment[gl]=Movemento da xanela +Comment[he]=מסגרת חלון +Comment[hu]=Ablakmozgatás +Comment[ia]=Movimento de fenestra +Comment[id]=Memindahkan Jendela +Comment[it]=Spostamento delle finestre +Comment[ko]=창 이동 +Comment[lt]=Lango judinimas +Comment[nb]=Vindusflytting +Comment[nds]=Finstern bewegen +Comment[nl]=Verplaatsen van vensters +Comment[nn]=Vindaugsflytting +Comment[pa]=ਵਿੰਡੋ ਏਧਰ-ਓਧਰ ਕਰੋ +Comment[pl]=Przesuwanie okien +Comment[pt]=Movimentação das Janelas +Comment[pt_BR]=Movimentação da janela +Comment[ru]=Перемещение окон +Comment[sk]=Presuny okien +Comment[sl]=Premikanje oken +Comment[sr]=Померање прозора +Comment[sr@ijekavian]=Померање прозора +Comment[sr@ijekavianlatin]=Pomeranje prozora +Comment[sr@latin]=Pomeranje prozora +Comment[sv]=Fönsterförflyttning +Comment[tr]=Pencere Hareketi +Comment[uk]=Пересування вікон +Comment[vi]=Di chuyển cửa sổ +Comment[x-test]=xxWindow Movingxx +Comment[zh_CN]=窗口移动 +Comment[zh_TW]=視窗移動 + +X-KDE-Keywords=moving,smart,cascade,maximize,maximise,snap zone,snap,border +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]=memindahkan,cerdas,riam,maksimalkan,maksimalkan,zona patah,patah,batas +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[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[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..ae3c0ed --- /dev/null +++ b/kcmkwin/kwinoptions/kwinoptions.desktop @@ -0,0 +1,189 @@ +[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[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 la ventana +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]=Window Behavior +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 fereastră +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[tg]=Холати тиреза +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=Window Actions and Behavior +Comment[bs]=Akcije i ponašanje prozora +Comment[ca]=Accions i comportament de les finestres +Comment[ca@valencia]=Accions i comportament de les finestres +Comment[cs]=Činnosti a chování oken +Comment[da]=Vindueshandlinger og -opførsel +Comment[de]=Fenster-Aktionen und -verhalten +Comment[el]=Ενέργειες και συμπεριφορά παραθύρου +Comment[en_GB]=Window Actions and Behaviour +Comment[es]=Acciones y comportamiento de las ventanas +Comment[et]=Akende toimingud ja käitumine +Comment[eu]=Leihoen ekintzak eta portaera +Comment[fi]=Ikkunoiden toiminnot ja toiminta +Comment[fr]=Actions et comportement des fenêtres +Comment[gl]=Comportamento e accións das xanelas +Comment[he]=התנהגויות ופעולות של חלונות +Comment[hu]=Ablakműveletek és működés +Comment[ia]=Comportamento e actiones de fenestra +Comment[id]=Aksi dan Perilaku Jendela +Comment[it]=Azioni e comportamento delle finestre +Comment[ja]=ウィンドウのアクションと挙動 +Comment[ko]=창 동작과 행동 +Comment[lt]=Lango veiksmai ir elgsena +Comment[nb]=Vindusoppførsel og handlinger +Comment[nds]=Finsterakschonen und -bedregen +Comment[nl]=Vensteracties en gedrag +Comment[nn]=Handlingar og åtferd for vindauge +Comment[pa]=ਵਿੰਡੋ ਕਾਰੀਆਂ ਅਤੇ ਰਵੱਈਆ +Comment[pl]=Działania i zachowania okien +Comment[pt]=Acções e Comportamento das Janelas +Comment[pt_BR]=Ações e comportamento das janelas +Comment[ru]=Настройка поведения окон +Comment[sk]=Akcie a správanie okien +Comment[sl]=Dejanja in obnašanje oken +Comment[sr]=Понашање прозора и радње над њима +Comment[sr@ijekavian]=Понашање прозора и радње над њима +Comment[sr@ijekavianlatin]=Ponašanje prozora i radnje nad njima +Comment[sr@latin]=Ponašanje prozora i radnje nad njima +Comment[sv]=Fönsteråtgärder och beteende +Comment[tr]=Pencere Eylem ve Davranışları +Comment[uk]=Реакція і поведінка вікон +Comment[x-test]=xxWindow 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[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,clic doble +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 jendela,aksi jendela,animasi,naikkan,naikkan otomatis,jendela,bingkai,bilah judul,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[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[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,titulkový pruh,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[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/main.cpp b/kcmkwin/kwinoptions/main.cpp new file mode 100644 index 0000000..6bb1a61 --- /dev/null +++ b/kcmkwin/kwinoptions/main.cpp @@ -0,0 +1,249 @@ +/* + * + * Copyright (c) 2001 Waldo Bastian + * + * 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. + */ + +#include "main.h" + +#include +//Added by qt3to4: +#include + +#include + +#include +#include +#include +#include +#include + +#include "mouse.h" +#include "windows.h" + +K_PLUGIN_FACTORY_DECLARATION(KWinOptionsFactory) + +class KFocusConfigStandalone : public KFocusConfig +{ + Q_OBJECT +public: + KFocusConfigStandalone(QWidget* parent, const QVariantList &) + : KFocusConfig(true, new KConfig("kwinrc"), parent) + {} +}; + +class KMovingConfigStandalone : public KMovingConfig +{ + Q_OBJECT +public: + KMovingConfigStandalone(QWidget* parent, const QVariantList &) + : KMovingConfig(true, new KConfig("kwinrc"), parent) + {} +}; + +class KAdvancedConfigStandalone : public KAdvancedConfig +{ + Q_OBJECT +public: + KAdvancedConfigStandalone(QWidget* parent, const QVariantList &) + : KAdvancedConfig(true, new KConfig("kwinrc"), parent) + {} +}; + +KWinOptions::KWinOptions(QWidget *parent, const QVariantList &) + : KCModule(parent) +{ + mConfig = new KConfig("kwinrc"); + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setMargin(0); + tab = new QTabWidget(this); + layout->addWidget(tab); + + mFocus = new KFocusConfig(false, mConfig, this); + mFocus->setObjectName(QLatin1String("KWin Focus Config")); + tab->addTab(mFocus, i18n("&Focus")); + connect(mFocus, SIGNAL(changed(bool)), this, SLOT(moduleChanged(bool))); + + mTitleBarActions = new KTitleBarActionsConfig(false, mConfig, this); + mTitleBarActions->setObjectName(QLatin1String("KWin TitleBar Actions")); + tab->addTab(mTitleBarActions, i18n("&Titlebar Actions")); + connect(mTitleBarActions, SIGNAL(changed(bool)), this, SLOT(moduleChanged(bool))); + + mWindowActions = new KWindowActionsConfig(false, mConfig, this); + mWindowActions->setObjectName(QLatin1String("KWin Window Actions")); + tab->addTab(mWindowActions, i18n("Window Actio&ns")); + connect(mWindowActions, SIGNAL(changed(bool)), this, SLOT(moduleChanged(bool))); + + mMoving = new KMovingConfig(false, mConfig, this); + mMoving->setObjectName(QLatin1String("KWin Moving")); + tab->addTab(mMoving, i18n("&Moving")); + connect(mMoving, SIGNAL(changed(bool)), this, SLOT(moduleChanged(bool))); + + mAdvanced = new KAdvancedConfig(false, mConfig, this); + mAdvanced->setObjectName(QLatin1String("KWin Advanced")); + tab->addTab(mAdvanced, i18n("Ad&vanced")); + connect(mAdvanced, SIGNAL(changed(bool)), this, SLOT(moduleChanged(bool))); + + 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); +} + +KWinOptions::~KWinOptions() +{ + delete mConfig; +} + +void KWinOptions::load() +{ + mConfig->reparseConfiguration(); + mFocus->load(); + mTitleBarActions->load(); + mWindowActions->load(); + mMoving->load(); + mAdvanced->load(); + emit KCModule::changed(false); +} + + +void KWinOptions::save() +{ + mFocus->save(); + mTitleBarActions->save(); + mWindowActions->save(); + mMoving->save(); + mAdvanced->save(); + + emit KCModule::changed(false); + // Send signal to kwin + mConfig->sync(); + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + + +} + + +void KWinOptions::defaults() +{ + mFocus->defaults(); + mTitleBarActions->defaults(); + mWindowActions->defaults(); + mMoving->defaults(); + mAdvanced->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.

"); +} + +void KWinOptions::moduleChanged(bool state) +{ + emit KCModule::changed(state); +} + +KActionsOptions::KActionsOptions(QWidget *parent, const QVariantList &) + : KCModule(parent) +{ + mConfig = new KConfig("kwinrc"); + + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setMargin(0); + tab = new QTabWidget(this); + layout->addWidget(tab); + + mTitleBarActions = new KTitleBarActionsConfig(false, mConfig, this); + mTitleBarActions->setObjectName(QLatin1String("KWin TitleBar Actions")); + tab->addTab(mTitleBarActions, i18n("&Titlebar Actions")); + connect(mTitleBarActions, SIGNAL(changed(bool)), this, SLOT(moduleChanged(bool))); + + mWindowActions = new KWindowActionsConfig(false, mConfig, this); + mWindowActions->setObjectName(QLatin1String("KWin Window Actions")); + tab->addTab(mWindowActions, i18n("Window Actio&ns")); + connect(mWindowActions, SIGNAL(changed(bool)), this, SLOT(moduleChanged(bool))); +} + +KActionsOptions::~KActionsOptions() +{ + delete mConfig; +} + +void KActionsOptions::load() +{ + mTitleBarActions->load(); + mWindowActions->load(); + emit KCModule::changed(false); +} + + +void KActionsOptions::save() +{ + mTitleBarActions->save(); + mWindowActions->save(); + + emit KCModule::changed(false); + // Send signal to kwin + mConfig->sync(); + // 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" +#include "moc_main.cpp" diff --git a/kcmkwin/kwinoptions/main.h b/kcmkwin/kwinoptions/main.h new file mode 100644 index 0000000..b7f28df --- /dev/null +++ b/kcmkwin/kwinoptions/main.h @@ -0,0 +1,99 @@ +/* + * main.h + * + * Copyright (c) 2001 Waldo Bastian + * + * Requires the Qt widget libraries, available at no cost at + * http://www.troll.no/ + * + * 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. + */ + + +#ifndef __MAIN_H__ +#define __MAIN_H__ + +#include +#include + +class KConfig; +class KFocusConfig; +class KTitleBarActionsConfig; +class KWindowActionsConfig; +class KAdvancedConfig; +class KMovingConfig; + +class KWinOptions : public KCModule +{ + Q_OBJECT + +public: + + KWinOptions(QWidget *parent, const QVariantList &args); + virtual ~KWinOptions(); + + void load(); + void save(); + void defaults(); + QString quickHelp() const; + + +protected Q_SLOTS: + + void moduleChanged(bool state); + + +private: + + QTabWidget *tab; + + KFocusConfig *mFocus; + KTitleBarActionsConfig *mTitleBarActions; + KWindowActionsConfig *mWindowActions; + KMovingConfig *mMoving; + KAdvancedConfig *mAdvanced; + + KConfig *mConfig; +}; + +class KActionsOptions : public KCModule +{ + Q_OBJECT + +public: + + KActionsOptions(QWidget *parent, const QVariantList &args); + virtual ~KActionsOptions(); + + void load(); + void save(); + void defaults(); + +protected Q_SLOTS: + + void moduleChanged(bool state); + + +private: + + QTabWidget *tab; + + KTitleBarActionsConfig *mTitleBarActions; + KWindowActionsConfig *mWindowActions; + + KConfig *mConfig; +}; + +#endif diff --git a/kcmkwin/kwinoptions/mouse.cpp b/kcmkwin/kwinoptions/mouse.cpp new file mode 100644 index 0000000..0d9678c --- /dev/null +++ b/kcmkwin/kwinoptions/mouse.cpp @@ -0,0 +1,570 @@ +/* + * + * Copyright (c) 1998 Matthias Ettrich + * + * 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. + */ + +#include "mouse.h" + +#include +#include + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include + + +namespace +{ + +char const * const cnf_Max[] = { + "MaximizeButtonLeftClickCommand", + "MaximizeButtonMiddleClickCommand", + "MaximizeButtonRightClickCommand", +}; + +char const * const tbl_Max[] = { + "Maximize", + "Maximize (vertical only)", + "Maximize (horizontal only)", + "" +}; + +QPixmap maxButtonPixmaps[3]; + +void createMaxButtonPixmaps() +{ + char const * maxButtonXpms[][3 + 13] = { + { + 0, 0, 0, + "...............", + ".......#.......", + "......###......", + ".....#####.....", + "..#....#....#..", + ".##....#....##.", + "###############", + ".##....#....##.", + "..#....#....#..", + ".....#####.....", + "......###......", + ".......#.......", + "..............." + }, + { + 0, 0, 0, + "...............", + ".......#.......", + "......###......", + ".....#####.....", + ".......#.......", + ".......#.......", + ".......#.......", + ".......#.......", + ".......#.......", + ".....#####.....", + "......###......", + ".......#.......", + "..............." + }, + { + 0, 0, 0, + "...............", + "...............", + "...............", + "...............", + "..#.........#..", + ".##.........##.", + "###############", + ".##.........##.", + "..#.........#..", + "...............", + "...............", + "...............", + "..............." + }, + }; + + QByteArray baseColor(". c " + KColorScheme(QPalette::Active, KColorScheme::View).background().color().name().toAscii()); + QByteArray textColor("# c " + KColorScheme(QPalette::Active, KColorScheme::View).foreground().color().name().toAscii()); + for (int t = 0; t < 3; ++t) { + maxButtonXpms[t][0] = "15 13 2 1"; + maxButtonXpms[t][1] = baseColor.constData(); + maxButtonXpms[t][2] = textColor.constData(); + maxButtonPixmaps[t] = QPixmap(maxButtonXpms[t]); + maxButtonPixmaps[t].setMask(maxButtonPixmaps[t].createHeuristicMask()); + } +} + +} // namespace + +KWinMouseConfigForm::KWinMouseConfigForm(QWidget *parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KWinActionsConfigForm::KWinActionsConfigForm(QWidget *parent) + : QWidget(parent) +{ + setupUi(parent); +} + +void KTitleBarActionsConfig::paletteChanged() +{ + createMaxButtonPixmaps(); + for (int i=0; i<3; ++i) { + m_ui->leftClickMaximizeButton->setItemIcon(i, maxButtonPixmaps[i]); + m_ui->middleClickMaximizeButton->setItemIcon(i, maxButtonPixmaps[i]); + m_ui->rightClickMaximizeButton->setItemIcon(i, maxButtonPixmaps[i]); + } + +} + +KTitleBarActionsConfig::KTitleBarActionsConfig(bool _standAlone, KConfig *_config, QWidget * parent) + : KCModule(parent), config(_config), standAlone(_standAlone) + , m_ui(new KWinMouseConfigForm(this)) +{ + // create the items for the maximize button actions + createMaxButtonPixmaps(); + for (int i=0; i<3; ++i) { + m_ui->leftClickMaximizeButton->addItem(maxButtonPixmaps[i], QString()); + m_ui->middleClickMaximizeButton->addItem(maxButtonPixmaps[i], QString()); + m_ui->rightClickMaximizeButton->addItem(maxButtonPixmaps[i], QString()); + } + createMaximizeButtonTooltips(m_ui->leftClickMaximizeButton); + createMaximizeButtonTooltips(m_ui->middleClickMaximizeButton); + createMaximizeButtonTooltips(m_ui->rightClickMaximizeButton); + + connect(m_ui->coTiDbl, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coTiAct1, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coTiAct2, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coTiAct3, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coTiAct4, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coTiInAct1, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coTiInAct2, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coTiInAct3, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->leftClickMaximizeButton, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->middleClickMaximizeButton, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->rightClickMaximizeButton, SIGNAL(activated(int)), SLOT(changed())); + + load(); +} + +KTitleBarActionsConfig::~KTitleBarActionsConfig() +{ + if (standAlone) + delete config; +} + +void KTitleBarActionsConfig::createMaximizeButtonTooltips(KComboBox *combo) +{ + combo->setItemData(0, i18n("Maximize"), Qt::ToolTipRole); + combo->setItemData(1, i18n("Maximize (vertical only)"), Qt::ToolTipRole); + combo->setItemData(2, i18n("Maximize (horizontal only)"), Qt::ToolTipRole); +} + +// do NOT change the texts below, they are written to config file +// and are not shown in the GUI +// they have to match the order of items in GUI elements though +const char* const tbl_TiDbl[] = { + "Maximize", + "Maximize (vertical only)", + "Maximize (horizontal only)", + "Minimize", + "Shade", + "Lower", + "Close", + "OnAllDesktops", + "Nothing", + "" +}; + +const char* const tbl_TiAc[] = { + "Raise", + "Lower", + "Toggle raise and lower", + "Minimize", + "Shade", + "Close", + "Operations menu", + "Start window tab drag", + "Nothing", + "" +}; + +const char* const tbl_TiInAc[] = { + "Activate and raise", + "Activate and lower", + "Activate", + "Raise", + "Lower", + "Toggle raise and lower", + "Minimize", + "Shade", + "Close", + "Operations menu", + "Start window tab drag", + "Nothing", + "" +}; + +const char* const tbl_Win[] = { + "Activate, raise and pass click", + "Activate and pass click", + "Activate", + "Activate and raise", + "" +}; + +const char* const tbl_WinWheel[] = { + "Scroll", + "Activate and scroll", + "Activate, raise and scroll", + "" +}; + +const char* const tbl_AllKey[] = { + "Meta", + "Alt", + "" +}; + +const char* const tbl_All[] = { + "Move", + "Activate, raise and move", + "Toggle raise and lower", + "Resize", + "Raise", + "Lower", + "Minimize", + "Decrease Opacity", + "Increase Opacity", + "Nothing", + "" +}; + +const char* const tbl_TiWAc[] = { + "Raise/Lower", + "Shade/Unshade", + "Maximize/Restore", + "Above/Below", + "Previous/Next Desktop", + "Change Opacity", + "Switch to Window Tab to the Left/Right", + "Nothing", + "" +}; + +const char* const tbl_AllW[] = { + "Raise/Lower", + "Shade/Unshade", + "Maximize/Restore", + "Above/Below", + "Previous/Next Desktop", + "Change Opacity", + "Switch to Window Tab to the Left/Right", + "Nothing", + "" +}; + +static const char* tbl_num_lookup(const char* const arr[], int pos) +{ + for (int i = 0; + arr[ i ][ 0 ] != '\0' && pos >= 0; + ++i) { + if (pos == 0) + return arr[ i ]; + --pos; + } + abort(); // should never happen this way +} + +static int tbl_txt_lookup(const char* const arr[], const char* txt) +{ + int pos = 0; + for (int i = 0; + arr[ i ][ 0 ] != '\0'; + ++i) { + if (qstricmp(txt, arr[ i ]) == 0) + return pos; + ++pos; + } + return 0; +} + +void KTitleBarActionsConfig::setComboText(KComboBox* combo, const char*txt) +{ + if (combo == m_ui->coTiDbl) + combo->setCurrentIndex(tbl_txt_lookup(tbl_TiDbl, txt)); + else if (combo == m_ui->coTiAct1 || combo == m_ui->coTiAct2 || combo == m_ui->coTiAct3) + combo->setCurrentIndex(tbl_txt_lookup(tbl_TiAc, txt)); + else if (combo == m_ui->coTiInAct1 || combo == m_ui->coTiInAct2 || combo == m_ui->coTiInAct3) + combo->setCurrentIndex(tbl_txt_lookup(tbl_TiInAc, txt)); + else if (combo == m_ui->coTiAct4) + combo->setCurrentIndex(tbl_txt_lookup(tbl_TiWAc, txt)); + else if (combo == m_ui->leftClickMaximizeButton || + combo == m_ui->middleClickMaximizeButton || + combo == m_ui->rightClickMaximizeButton) { + combo->setCurrentIndex(tbl_txt_lookup(tbl_Max, txt)); + } else + abort(); +} + +const char* KTitleBarActionsConfig::functionTiDbl(int i) +{ + return tbl_num_lookup(tbl_TiDbl, i); +} + +const char* KTitleBarActionsConfig::functionTiAc(int i) +{ + return tbl_num_lookup(tbl_TiAc, i); +} + +const char* KTitleBarActionsConfig::functionTiInAc(int i) +{ + return tbl_num_lookup(tbl_TiInAc, i); +} + +const char* KTitleBarActionsConfig::functionTiWAc(int i) +{ + return tbl_num_lookup(tbl_TiWAc, i); +} + +const char* KTitleBarActionsConfig::functionMax(int i) +{ + return tbl_num_lookup(tbl_Max, i); +} + +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) +{ + if (ev->type() == QEvent::PaletteChange) { + paletteChanged(); + } + ev->accept(); +} + + +void KTitleBarActionsConfig::load() +{ + KConfigGroup windowsConfig(config, "Windows"); + setComboText(m_ui->coTiDbl, windowsConfig.readEntry("TitlebarDoubleClickCommand", "Maximize").toAscii()); + setComboText(m_ui->leftClickMaximizeButton, windowsConfig.readEntry(cnf_Max[0], tbl_Max[0]).toAscii()); + setComboText(m_ui->middleClickMaximizeButton, windowsConfig.readEntry(cnf_Max[1], tbl_Max[1]).toAscii()); + setComboText(m_ui->rightClickMaximizeButton, windowsConfig.readEntry(cnf_Max[2], tbl_Max[2]).toAscii()); + + KConfigGroup cg(config, "MouseBindings"); + setComboText(m_ui->coTiAct1, cg.readEntry("CommandActiveTitlebar1", "Raise").toAscii()); + setComboText(m_ui->coTiAct2, cg.readEntry("CommandActiveTitlebar2", "Start Window Tab Drag").toAscii()); + setComboText(m_ui->coTiAct3, cg.readEntry("CommandActiveTitlebar3", "Operations menu").toAscii()); + setComboText(m_ui->coTiAct4, cg.readEntry("CommandTitlebarWheel", "Switch to Window Tab to the Left/Right").toAscii()); + setComboText(m_ui->coTiInAct1, cg.readEntry("CommandInactiveTitlebar1", "Activate and raise").toAscii()); + setComboText(m_ui->coTiInAct2, cg.readEntry("CommandInactiveTitlebar2", "Start Window Tab Drag").toAscii()); + setComboText(m_ui->coTiInAct3, cg.readEntry("CommandInactiveTitlebar3", "Operations menu").toAscii()); +} + +void KTitleBarActionsConfig::save() +{ + KConfigGroup windowsConfig(config, "Windows"); + windowsConfig.writeEntry("TitlebarDoubleClickCommand", functionTiDbl(m_ui->coTiDbl->currentIndex())); + windowsConfig.writeEntry(cnf_Max[0], functionMax(m_ui->leftClickMaximizeButton->currentIndex())); + windowsConfig.writeEntry(cnf_Max[1], functionMax(m_ui->middleClickMaximizeButton->currentIndex())); + windowsConfig.writeEntry(cnf_Max[2], functionMax(m_ui->rightClickMaximizeButton->currentIndex())); + + KConfigGroup cg(config, "MouseBindings"); + cg.writeEntry("CommandActiveTitlebar1", functionTiAc(m_ui->coTiAct1->currentIndex())); + cg.writeEntry("CommandActiveTitlebar2", functionTiAc(m_ui->coTiAct2->currentIndex())); + cg.writeEntry("CommandActiveTitlebar3", functionTiAc(m_ui->coTiAct3->currentIndex())); + cg.writeEntry("CommandInactiveTitlebar1", functionTiInAc(m_ui->coTiInAct1->currentIndex())); + cg.writeEntry("CommandTitlebarWheel", functionTiWAc(m_ui->coTiAct4->currentIndex())); + cg.writeEntry("CommandInactiveTitlebar2", functionTiInAc(m_ui->coTiInAct2->currentIndex())); + cg.writeEntry("CommandInactiveTitlebar3", functionTiInAc(m_ui->coTiInAct3->currentIndex())); + + if (standAlone) { + config->sync(); + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + + } +} + +void KTitleBarActionsConfig::defaults() +{ + setComboText(m_ui->coTiDbl, "Maximize"); + setComboText(m_ui->coTiAct1, "Raise"); + setComboText(m_ui->coTiAct2, "Start Window Tab Drag"); + setComboText(m_ui->coTiAct3, "Operations menu"); + setComboText(m_ui->coTiAct4, "Switch to Window Tab to the Left/Right"); + setComboText(m_ui->coTiInAct1, "Activate and raise"); + setComboText(m_ui->coTiInAct2, "Start Window Tab Drag"); + setComboText(m_ui->coTiInAct3, "Operations menu"); + setComboText(m_ui->leftClickMaximizeButton, tbl_Max[0]); + setComboText(m_ui->middleClickMaximizeButton, tbl_Max[1]); + setComboText(m_ui->rightClickMaximizeButton, tbl_Max[2]); +} + + +KWindowActionsConfig::KWindowActionsConfig(bool _standAlone, KConfig *_config, QWidget * parent) + : KCModule(parent), config(_config), standAlone(_standAlone) + , m_ui(new KWinActionsConfigForm(this)) +{ + connect(m_ui->coWin1, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coWin2, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coWin3, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coWinWheel, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coAllKey, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coAll1, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coAll2, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coAll3, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->coAllW, SIGNAL(activated(int)), SLOT(changed())); + load(); +} + +KWindowActionsConfig::~KWindowActionsConfig() +{ + if (standAlone) + delete config; +} + +void KWindowActionsConfig::setComboText(KComboBox* combo, const char*txt) +{ + if (combo == m_ui->coWin1 || combo == m_ui->coWin2 || combo == m_ui->coWin3) + combo->setCurrentIndex(tbl_txt_lookup(tbl_Win, txt)); + else if (combo == m_ui->coWinWheel) + combo->setCurrentIndex(tbl_txt_lookup(tbl_WinWheel, txt)); + else if (combo == m_ui->coAllKey) + combo->setCurrentIndex(tbl_txt_lookup(tbl_AllKey, txt)); + else if (combo == m_ui->coAll1 || combo == m_ui->coAll2 || combo == m_ui->coAll3) + combo->setCurrentIndex(tbl_txt_lookup(tbl_All, txt)); + else if (combo == m_ui->coAllW) + combo->setCurrentIndex(tbl_txt_lookup(tbl_AllW, txt)); + else + abort(); +} + +const char* KWindowActionsConfig::functionWin(int i) +{ + return tbl_num_lookup(tbl_Win, i); +} + +const char* KWindowActionsConfig::functionWinWheel(int i) +{ + return tbl_num_lookup(tbl_WinWheel, i); +} + +const char* KWindowActionsConfig::functionAllKey(int i) +{ + return tbl_num_lookup(tbl_AllKey, i); +} + +const char* KWindowActionsConfig::functionAll(int i) +{ + return tbl_num_lookup(tbl_All, i); +} + +const char* KWindowActionsConfig::functionAllW(int i) +{ + return tbl_num_lookup(tbl_AllW, i); +} + +void KWindowActionsConfig::showEvent(QShowEvent *ev) +{ + if (!standAlone) { + QWidget::showEvent(ev); + return; + } + KCModule::showEvent(ev); +} + +void KWindowActionsConfig::load() +{ + KConfigGroup cg(config, "MouseBindings"); + setComboText(m_ui->coWin1, cg.readEntry("CommandWindow1", "Activate, raise and pass click").toAscii()); + setComboText(m_ui->coWin2, cg.readEntry("CommandWindow2", "Activate and pass click").toAscii()); + setComboText(m_ui->coWin3, cg.readEntry("CommandWindow3", "Activate and pass click").toAscii()); + setComboText(m_ui->coWinWheel, cg.readEntry("CommandWindowWheel", "Scroll").toAscii()); + setComboText(m_ui->coAllKey, cg.readEntry("CommandAllKey", "Alt").toAscii()); + setComboText(m_ui->coAll1, cg.readEntry("CommandAll1", "Move").toAscii()); + setComboText(m_ui->coAll2, cg.readEntry("CommandAll2", "Toggle raise and lower").toAscii()); + setComboText(m_ui->coAll3, cg.readEntry("CommandAll3", "Resize").toAscii()); + setComboText(m_ui->coAllW, cg.readEntry("CommandAllWheel", "Nothing").toAscii()); +} + +void KWindowActionsConfig::save() +{ + KConfigGroup cg(config, "MouseBindings"); + cg.writeEntry("CommandWindow1", functionWin(m_ui->coWin1->currentIndex())); + cg.writeEntry("CommandWindow2", functionWin(m_ui->coWin2->currentIndex())); + cg.writeEntry("CommandWindow3", functionWin(m_ui->coWin3->currentIndex())); + cg.writeEntry("CommandWindowWheel", functionWinWheel(m_ui->coWinWheel->currentIndex())); + cg.writeEntry("CommandAllKey", functionAllKey(m_ui->coAllKey->currentIndex())); + cg.writeEntry("CommandAll1", functionAll(m_ui->coAll1->currentIndex())); + cg.writeEntry("CommandAll2", functionAll(m_ui->coAll2->currentIndex())); + cg.writeEntry("CommandAll3", functionAll(m_ui->coAll3->currentIndex())); + cg.writeEntry("CommandAllWheel", functionAllW(m_ui->coAllW->currentIndex())); + + if (standAlone) { + config->sync(); + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + } +} + +void KWindowActionsConfig::defaults() +{ + setComboText(m_ui->coWin1, "Activate, raise and pass click"); + setComboText(m_ui->coWin2, "Activate and pass click"); + setComboText(m_ui->coWin3, "Activate and pass click"); + setComboText(m_ui->coWinWheel, "Scroll"); + setComboText(m_ui->coAllKey, "Alt"); + setComboText(m_ui->coAll1, "Move"); + setComboText(m_ui->coAll2, "Toggle raise and lower"); + setComboText(m_ui->coAll3, "Resize"); + setComboText(m_ui->coAllW, "Nothing"); +} diff --git a/kcmkwin/kwinoptions/mouse.h b/kcmkwin/kwinoptions/mouse.h new file mode 100644 index 0000000..c7a1e3c --- /dev/null +++ b/kcmkwin/kwinoptions/mouse.h @@ -0,0 +1,132 @@ +/* + * mouse.h + * + * Copyright (c) 1998 Matthias Ettrich + * + * + * 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. + */ + +#ifndef __KKWMMOUSECONFIG_H__ +#define __KKWMMOUSECONFIG_H__ + +class KConfig; + +#include +#include +#include + +#include "ui_actions.h" +#include "ui_mouse.h" + +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, KConfig *_config, QWidget *parent); + ~KTitleBarActionsConfig(); + + void load(); + void save(); + void defaults(); + +protected: + void showEvent(QShowEvent *ev); + void changeEvent(QEvent *ev); + +public Q_SLOTS: + void changed() { + emit KCModule::changed(true); + } + +private: + + KConfig *config; + bool standAlone; + + KWinMouseConfigForm *m_ui; + + const char* functionTiDbl(int); + const char* functionTiAc(int); + const char* functionTiWAc(int); + const char* functionTiInAc(int); + const char* functionMax(int); + + void setComboText(KComboBox* combo, const char* text); + void createMaximizeButtonTooltips(KComboBox* combo); + const char* fixup(const char* s); + +private Q_SLOTS: + void paletteChanged(); + +}; + +class KWindowActionsConfig : public KCModule +{ + Q_OBJECT + +public: + + KWindowActionsConfig(bool _standAlone, KConfig *_config, QWidget *parent); + ~KWindowActionsConfig(); + + void load(); + void save(); + void defaults(); + +protected: + void showEvent(QShowEvent *ev); + +public Q_SLOTS: + void changed() { + emit KCModule::changed(true); + } + +private: + KConfig *config; + bool standAlone; + + KWinActionsConfigForm *m_ui; + + const char* functionWin(int); + const char* functionWinWheel(int); + const char* functionAllKey(int); + const char* functionAll(int); + const char* functionAllW(int); + + void setComboText(KComboBox* combo, const char* text); + const char* fixup(const char* s); +}; + +#endif + diff --git a/kcmkwin/kwinoptions/mouse.ui b/kcmkwin/kwinoptions/mouse.ui new file mode 100644 index 0000000..a2f1aba --- /dev/null +++ b/kcmkwin/kwinoptions/mouse.ui @@ -0,0 +1,836 @@ + + + KWinMouseConfigForm + + + + 0 + 0 + 696 + 416 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + &Double-click: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + coTiDbl + + + + + + + + 0 + 0 + + + + Behavior on <em>double</em> click into the titlebar. + + + + Maximize + + + + + Maximize (vertical only) + + + + + Maximize (horizontal only) + + + + + Minimize + + + + + Shade + + + + + Lower + + + + + Close + + + + + On All Desktops + + + + + Nothing + + + + + + + + Wheel event: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + coTiAct4 + + + + + + + + 0 + 0 + + + + Handle mouse wheel events + + + + Raise/Lower + + + + + Shade/Unshade + + + + + Maximize/Restore + + + + + Keep Above/Below + + + + + Move to Previous/Next Desktop + + + + + Change Opacity + + + + + Switch to Window Tab to the Left/Right + + + + + Nothing + + + + + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + In this column you can customize mouse clicks into the titlebar or the frame of an active window. + + + Active + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + In this column you can customize mouse clicks into the titlebar or the frame of an inactive window. + + + Inactive + + + Qt::AlignCenter + + + + + + + In this row you can customize left click behavior when clicking into the titlebar or the frame. + + + Left button: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>active</em> window. + + + + Raise + + + + + Lower + + + + + Toggle Raise & Lower + + + + + Minimize + + + + + Shade + + + + + Close + + + + + Operations Menu + + + + + Start Window Tab Drag + + + + + Nothing + + + + + + + + + 0 + 0 + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>inactive</em> window. + + + + Activate & Raise + + + + + Activate & Lower + + + + + Activate + + + + + Raise + + + + + Lower + + + + + Toggle Raise & Lower + + + + + Minimize + + + + + Shade + + + + + Close + + + + + Operations Menu + + + + + Start Window Tab Drag + + + + + Nothing + + + + + + + + In this row you can customize middle click behavior when clicking into the titlebar or the frame. + + + Middle button: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Behavior on <em>middle</em> click into the titlebar or frame of an <em>active</em> window. + + + + Raise + + + + + Lower + + + + + Toggle Raise & Lower + + + + + Minimize + + + + + Shade + + + + + Close + + + + + Operations Menu + + + + + Start Window Tab Drag + + + + + Nothing + + + + + + + + + 0 + 0 + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>inactive</em> window. + + + + Activate & Raise + + + + + Activate & Lower + + + + + Activate + + + + + Raise + + + + + Lower + + + + + Toggle Raise & Lower + + + + + Minimize + + + + + Shade + + + + + Close + + + + + Operations Menu + + + + + Start Window Tab Drag + + + + + Nothing + + + + + + + + In this row you can customize right click behavior when clicking into the titlebar or the frame. + + + Right button: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + 0 + 0 + + + + Behavior on <em>right</em> click into the titlebar or frame of an <em>active</em> window. + + + + Raise + + + + + Lower + + + + + Toggle Raise & Lower + + + + + Minimize + + + + + Shade + + + + + Close + + + + + Operations Menu + + + + + Start Window Tab Drag + + + + + Nothing + + + + + + + + + 0 + 0 + + + + Behavior on <em>left</em> click into the titlebar or frame of an <em>inactive</em> window. + + + + Activate & Raise + + + + + Activate & Lower + + + + + Activate + + + + + Raise + + + + + Lower + + + + + Toggle Raise & Lower + + + + + Minimize + + + + + Shade + + + + + Close + + + + + Operations Menu + + + + + Start Window Tab Drag + + + + + Nothing + + + + + + + + Qt::Horizontal + + + + + + + + + Behavior on <em>left</em> click onto the maximize button. + + + Left button: + + + Qt::AlignCenter + + + leftClickMaximizeButton + + + + + + + Behavior on <em>middle</em> click onto the maximize button. + + + Middle button: + + + Qt::AlignCenter + + + middleClickMaximizeButton + + + + + + + Behavior on <em>right</em> click onto the maximize button. + + + Right button: + + + Qt::AlignCenter + + + rightClickMaximizeButton + + + + + + + + 0 + 0 + + + + Behavior on <em>left</em> click onto the maximize button. + + + + + + + + 0 + 0 + + + + Behavior on <em>middle</em> click onto the maximize button. + + + + + + + + 0 + 0 + + + + Behavior on <em>right</em> click onto the maximize button. + + + + + + + + + Qt::Horizontal + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 75 + true + + + + Titlebar & Frame + + + + + + + + 75 + true + + + + Titlebar + + + + + + + + 75 + true + + + + Maximize Button + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+
+ + coTiDbl + coTiAct4 + coTiAct1 + coTiInAct1 + coTiAct2 + coTiInAct2 + coTiAct3 + coTiInAct3 + + + +
diff --git a/kcmkwin/kwinoptions/moving.ui b/kcmkwin/kwinoptions/moving.ui new file mode 100644 index 0000000..eb04689 --- /dev/null +++ b/kcmkwin/kwinoptions/moving.ui @@ -0,0 +1,270 @@ + + + KWinMovingConfigForm + + + + 0 + 0 + 624 + 354 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + 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. + + + Snap windows onl&y when overlapping + + + + + + + Qt::Horizontal + + + + + + + + 0 + 0 + + + + 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. + + + 10 + + + 0 + + + 100 + + + pixel + + + no center snap zone + + + + + + + Qt::Horizontal + + + + 107 + 20 + + + + + + + + &Border snap zone: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + borderSnap + + + + + + + &Center snap zone: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + centerSnap + + + + + + + + 0 + 0 + + + + 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. + + + 10 + + + 0 + + + 100 + + + pixel + + + no window snap zone + + + + + + + + 0 + 0 + + + + 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. + + + 10 + + + 0 + + + 100 + + + pixel + + + no border snap zone + + + + + + + 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 window &geometry when moving or resizing + + + + + + + &Window snap zone: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + windowSnap + + + + + + + Qt::Horizontal + + + + 90 + 20 + + + + + + + + Qt::Horizontal + + + + + + + + 75 + true + + + + Windows + + + + + + + + 75 + true + + + + Snap Zones + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Vertical + + + + 20 + 112 + + + + + + + + + diff --git a/kcmkwin/kwinoptions/windows.cpp b/kcmkwin/kwinoptions/windows.cpp new file mode 100644 index 0000000..3dbe15e --- /dev/null +++ b/kcmkwin/kwinoptions/windows.cpp @@ -0,0 +1,654 @@ +/* + * windows.cpp + * + * Copyright (c) 1997 Patrick Dowler dowler@morgul.fsh.uvic.ca + * Copyright (c) 2001 Waldo Bastian bastian@kde.org + * + * 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. + * + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include "windows.h" +#include +#include + +// kwin config keywords +#define KWIN_FOCUS "FocusPolicy" +#define KWIN_PLACEMENT "Placement" +#define KWIN_GEOMETRY "GeometryTip" +#define KWIN_AUTORAISE_INTERVAL "AutoRaiseInterval" +#define KWIN_AUTORAISE "AutoRaise" +#define KWIN_DELAYFOCUS_INTERVAL "DelayFocusInterval" +#define KWIN_CLICKRAISE "ClickRaise" +#define KWIN_SHADEHOVER "ShadeHover" +#define KWIN_SHADEHOVER_INTERVAL "ShadeHoverInterval" +#define KWIN_FOCUS_STEALING "FocusStealingPreventionLevel" +#define KWIN_HIDE_UTILITY "HideUtilityWindowsForInactive" +#define KWIN_INACTIVE_SKIP_TASKBAR "InactiveTabsSkipTaskbar" +#define KWIN_AUTOGROUP_SIMILAR "AutogroupSimilarWindows" +#define KWIN_AUTOGROUP_FOREGROUND "AutogroupInForeground" +#define KWIN_SEPARATE_SCREEN_FOCUS "SeparateScreenFocus" +#define KWIN_ACTIVE_MOUSE_SCREEN "ActiveMouseScreen" + +//CT 15mar 98 - magics +#define KWM_BRDR_SNAP_ZONE "BorderSnapZone" +#define KWM_BRDR_SNAP_ZONE_DEFAULT 10 +#define KWM_WNDW_SNAP_ZONE "WindowSnapZone" +#define KWM_WNDW_SNAP_ZONE_DEFAULT 10 +#define KWM_CNTR_SNAP_ZONE "CenterSnapZone" +#define KWM_CNTR_SNAP_ZONE_DEFAULT 0 + +#define MAX_BRDR_SNAP 100 +#define MAX_WNDW_SNAP 100 +#define MAX_CNTR_SNAP 100 +#define MAX_EDGE_RES 1000 + +#define CLICK_TO_FOCUS 0 +#define FOCUS_FOLLOWS_MOUSE 2 +#define FOCUS_UNDER_MOUSE 4 +#define FOCUS_STRICTLY_UNDER_MOUSE 5 + + +KFocusConfig::~KFocusConfig() +{ + if (standAlone) + delete config; +} + +KWinFocusConfigForm::KWinFocusConfigForm(QWidget* parent) + : QWidget(parent) +{ + setupUi(parent); +} + +// removed the LCD display over the slider - this is not good GUI design :) RNolden 051701 +KFocusConfig::KFocusConfig(bool _standAlone, KConfig *_config, QWidget * parent) + : KCModule(parent), config(_config), standAlone(_standAlone) + , m_ui(new KWinFocusConfigForm(this)) +{ + connect(m_ui->focusStealing, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->windowFocusPolicy, SIGNAL(valueChanged(int)), SLOT(changed())); + connect(m_ui->windowFocusPolicy, SIGNAL(valueChanged(int)), this, SLOT(focusPolicyChanged())); + connect(m_ui->windowFocusPolicy, SIGNAL(valueChanged(int)), this, SLOT(setDelayFocusEnabled())); + connect(m_ui->windowFocusPolicy, SIGNAL(valueChanged(int)), this, SLOT(updateActiveMouseScreen())); + connect(m_ui->autoRaiseOn, SIGNAL(clicked()), SLOT(changed())); + connect(m_ui->autoRaiseOn, SIGNAL(toggled(bool)), SLOT(autoRaiseOnTog(bool))); + connect(m_ui->clickRaiseOn, SIGNAL(clicked()), SLOT(changed())); + connect(m_ui->autoRaise, SIGNAL(valueChanged(int)), SLOT(changed())); + connect(m_ui->delayFocus, SIGNAL(valueChanged(int)), SLOT(changed())); + connect(m_ui->separateScreenFocus, SIGNAL(clicked()), SLOT(changed())); + connect(m_ui->activeMouseScreen, SIGNAL(clicked()), SLOT(changed())); + + connect(QApplication::desktop(), SIGNAL(screenCountChanged(int)), SLOT(updateMultiScreen())); + updateMultiScreen(); + + load(); +} + +void KFocusConfig::updateMultiScreen() +{ + m_ui->multiscreenBox->setVisible(QApplication::desktop()->screenCount() > 1); +} + + +int KFocusConfig::getFocus() +{ + int policy = m_ui->windowFocusPolicy->value(); + if (policy == 1 || policy == 3) + --policy; // fix the NextFocusPrefersMouse condition + return policy; +} + +void KFocusConfig::setFocus(int foc) +{ + m_ui->windowFocusPolicy->setValue(foc); + + // this will disable/hide the auto raise delay widget if focus==click + focusPolicyChanged(); +} + +void KFocusConfig::setAutoRaiseInterval(int tb) +{ + m_ui->autoRaise->setValue(tb); +} + +void KFocusConfig::setDelayFocusInterval(int tb) +{ + m_ui->delayFocus->setValue(tb); +} + +int KFocusConfig::getAutoRaiseInterval() +{ + return m_ui->autoRaise->value(); +} + +int KFocusConfig::getDelayFocusInterval() +{ + return m_ui->delayFocus->value(); +} + +void KFocusConfig::setAutoRaise(bool on) +{ + m_ui->autoRaiseOn->setChecked(on); +} + +void KFocusConfig::setClickRaise(bool on) +{ + m_ui->clickRaiseOn->setChecked(on); +} + +void KFocusConfig::focusPolicyChanged() +{ + int policyIndex = getFocus(); + + // the auto raise related widgets are: autoRaise + m_ui->autoRaiseOn->setEnabled(policyIndex != CLICK_TO_FOCUS); + autoRaiseOnTog(policyIndex != CLICK_TO_FOCUS && m_ui->autoRaiseOn->isChecked()); + + m_ui->focusStealing->setDisabled(policyIndex == FOCUS_UNDER_MOUSE || policyIndex == FOCUS_STRICTLY_UNDER_MOUSE); + m_ui->focusStealingLabel->setEnabled(m_ui->focusStealing->isEnabled()); + + setDelayFocusEnabled(); + +} + +void KFocusConfig::setDelayFocusEnabled() +{ + int policyIndex = getFocus(); + + // the delayed focus related widgets are: delayFocus + m_ui->delayFocusOnLabel->setEnabled(policyIndex != CLICK_TO_FOCUS); + delayFocusOnTog(policyIndex != CLICK_TO_FOCUS); +} + +void KFocusConfig::autoRaiseOnTog(bool a) +{ + m_ui->autoRaise->setEnabled(a); + m_ui->clickRaiseOn->setEnabled(!a); +} + +void KFocusConfig::delayFocusOnTog(bool a) +{ + m_ui->delayFocus->setEnabled(a); +} + +void KFocusConfig::setFocusStealing(int l) +{ + l = qMax(0, qMin(4, l)); + m_ui->focusStealing->setCurrentIndex(l); +} + +void KFocusConfig::setSeparateScreenFocus(bool s) +{ + m_ui->separateScreenFocus->setChecked(s); +} + +void KFocusConfig::setActiveMouseScreen(bool a) +{ + m_ui->activeMouseScreen->setChecked(a); +} + +void KFocusConfig::updateActiveMouseScreen() +{ + // on by default for non click to focus policies + KConfigGroup cfg(config, "Windows"); + if (!cfg.hasKey(KWIN_ACTIVE_MOUSE_SCREEN)) + setActiveMouseScreen(getFocus() != 0); +} + +void KFocusConfig::showEvent(QShowEvent *ev) +{ + if (!standAlone) { + QWidget::showEvent(ev); + return; + } + KCModule::showEvent(ev); +} + +void KFocusConfig::load(void) +{ + QString key; + + KConfigGroup cg(config, "Windows"); + + const bool focusNextToMouse = cg.readEntry("NextFocusPrefersMouse", false); + + key = cg.readEntry(KWIN_FOCUS); + if (key == "ClickToFocus") + setFocus(CLICK_TO_FOCUS + focusNextToMouse); + else if (key == "FocusFollowsMouse") + setFocus(FOCUS_FOLLOWS_MOUSE + focusNextToMouse); + else if (key == "FocusUnderMouse") + setFocus(FOCUS_UNDER_MOUSE); + else if (key == "FocusStrictlyUnderMouse") + setFocus(FOCUS_STRICTLY_UNDER_MOUSE); + + int k = cg.readEntry(KWIN_AUTORAISE_INTERVAL, 750); + setAutoRaiseInterval(k); + + k = cg.readEntry(KWIN_DELAYFOCUS_INTERVAL, 300); + setDelayFocusInterval(k); + + setAutoRaise(cg.readEntry(KWIN_AUTORAISE, false)); + setClickRaise(cg.readEntry(KWIN_CLICKRAISE, true)); + focusPolicyChanged(); // this will disable/hide the auto raise delay widget if focus==click + + setSeparateScreenFocus(cg.readEntry(KWIN_SEPARATE_SCREEN_FOCUS, false)); + // on by default for non click to focus policies + setActiveMouseScreen(cg.readEntry(KWIN_ACTIVE_MOUSE_SCREEN, getFocus() != 0)); + +// setFocusStealing( cg.readEntry(KWIN_FOCUS_STEALING, 2 )); + // TODO default to low for now + setFocusStealing(cg.readEntry(KWIN_FOCUS_STEALING, 1)); + + + emit KCModule::changed(false); +} + +void KFocusConfig::save(void) +{ + int v; + + KConfigGroup cg(config, "Windows"); + + v = getFocus(); + if (v == CLICK_TO_FOCUS) + cg.writeEntry(KWIN_FOCUS, "ClickToFocus"); + else if (v == FOCUS_UNDER_MOUSE) + cg.writeEntry(KWIN_FOCUS, "FocusUnderMouse"); + else if (v == FOCUS_STRICTLY_UNDER_MOUSE) + cg.writeEntry(KWIN_FOCUS, "FocusStrictlyUnderMouse"); + else + cg.writeEntry(KWIN_FOCUS, "FocusFollowsMouse"); + + cg.writeEntry("NextFocusPrefersMouse", v != m_ui->windowFocusPolicy->value()); + + v = getAutoRaiseInterval(); + if (v < 0) v = 0; + cg.writeEntry(KWIN_AUTORAISE_INTERVAL, v); + + v = getDelayFocusInterval(); + if (v < 0) v = 0; + cg.writeEntry(KWIN_DELAYFOCUS_INTERVAL, v); + + cg.writeEntry(KWIN_AUTORAISE, m_ui->autoRaiseOn->isChecked()); + + cg.writeEntry(KWIN_CLICKRAISE, m_ui->clickRaiseOn->isChecked()); + + cg.writeEntry(KWIN_SEPARATE_SCREEN_FOCUS, m_ui->separateScreenFocus->isChecked()); + cg.writeEntry(KWIN_ACTIVE_MOUSE_SCREEN, m_ui->activeMouseScreen->isChecked()); + + cg.writeEntry(KWIN_FOCUS_STEALING, m_ui->focusStealing->currentIndex()); + + cg.writeEntry(KWIN_SEPARATE_SCREEN_FOCUS, m_ui->separateScreenFocus->isChecked()); + cg.writeEntry(KWIN_ACTIVE_MOUSE_SCREEN, m_ui->activeMouseScreen->isChecked()); + + + if (standAlone) { + config->sync(); + // 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() +{ + setAutoRaiseInterval(0); + setDelayFocusInterval(0); + setFocus(CLICK_TO_FOCUS); + setAutoRaise(false); + setClickRaise(true); + setSeparateScreenFocus(false); + +// setFocusStealing(2); + // TODO default to low for now + setFocusStealing(1); + + // on by default for non click to focus policies + setActiveMouseScreen(getFocus() != 0); + setDelayFocusEnabled(); + emit KCModule::changed(true); +} + +KWinAdvancedConfigForm::KWinAdvancedConfigForm(QWidget* parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KAdvancedConfig::~KAdvancedConfig() +{ + if (standAlone) + delete config; +} + +KAdvancedConfig::KAdvancedConfig(bool _standAlone, KConfig *_config, QWidget *parent) + : KCModule(parent), config(_config), standAlone(_standAlone) + , m_ui(new KWinAdvancedConfigForm(this)) +{ + m_ui->placementCombo->setItemData(0, "Smart"); + m_ui->placementCombo->setItemData(1, "Maximizing"); + m_ui->placementCombo->setItemData(2, "Cascade"); + m_ui->placementCombo->setItemData(3, "Random"); + m_ui->placementCombo->setItemData(4, "Centered"); + m_ui->placementCombo->setItemData(5, "ZeroCornered"); + m_ui->placementCombo->setItemData(6, "UnderMouse"); + + connect(m_ui->shadeHoverOn, SIGNAL(toggled(bool)), this, SLOT(shadeHoverChanged(bool))); + connect(m_ui->inactiveTabsSkipTaskbar, SIGNAL(toggled(bool)), SLOT(changed())); + connect(m_ui->autogroupSimilarWindows, SIGNAL(toggled(bool)), SLOT(changed())); + connect(m_ui->autogroupInForeground, SIGNAL(toggled(bool)), SLOT(changed())); + connect(m_ui->shadeHoverOn, SIGNAL(toggled(bool)), SLOT(changed())); + connect(m_ui->shadeHover, SIGNAL(valueChanged(int)), SLOT(changed())); + connect(m_ui->placementCombo, SIGNAL(activated(int)), SLOT(changed())); + connect(m_ui->hideUtilityWindowsForInactive, SIGNAL(toggled(bool)), SLOT(changed())); + m_ui->inactiveTabsSkipTaskbar->setVisible(false); // TODO: We want translations in case this is fixed... + load(); + +} + +void KAdvancedConfig::setShadeHover(bool on) +{ + m_ui->shadeHoverOn->setChecked(on); + m_ui->shadeHoverLabel->setEnabled(on); + m_ui->shadeHover->setEnabled(on); +} + +void KAdvancedConfig::setShadeHoverInterval(int k) +{ + m_ui->shadeHover->setValue(k); +} + +int KAdvancedConfig::getShadeHoverInterval() +{ + + return m_ui->shadeHover->value(); +} + +void KAdvancedConfig::shadeHoverChanged(bool a) +{ + m_ui->shadeHoverLabel->setEnabled(a); + m_ui->shadeHover->setEnabled(a); +} + +void KAdvancedConfig::showEvent(QShowEvent *ev) +{ + if (!standAlone) { + QWidget::showEvent(ev); + return; + } + KCModule::showEvent(ev); +} + +void KAdvancedConfig::load(void) +{ + KConfigGroup cg(config, "Windows"); + + setShadeHover(cg.readEntry(KWIN_SHADEHOVER, false)); + setShadeHoverInterval(cg.readEntry(KWIN_SHADEHOVER_INTERVAL, 250)); + + QString key; + key = cg.readEntry(KWIN_PLACEMENT); + int idx = m_ui->placementCombo->findData(key); + if (idx < 0) + idx = m_ui->placementCombo->findData("Smart"); + m_ui->placementCombo->setCurrentIndex(idx); + + setHideUtilityWindowsForInactive(cg.readEntry(KWIN_HIDE_UTILITY, true)); + setInactiveTabsSkipTaskbar(cg.readEntry(KWIN_INACTIVE_SKIP_TASKBAR, false)); + setAutogroupSimilarWindows(cg.readEntry(KWIN_AUTOGROUP_SIMILAR, false)); + setAutogroupInForeground(cg.readEntry(KWIN_AUTOGROUP_FOREGROUND, true)); + + emit KCModule::changed(false); +} + +void KAdvancedConfig::save(void) +{ + int v; + + KConfigGroup cg(config, "Windows"); + cg.writeEntry(KWIN_SHADEHOVER, m_ui->shadeHoverOn->isChecked()); + + v = getShadeHoverInterval(); + if (v < 0) v = 0; + cg.writeEntry(KWIN_SHADEHOVER_INTERVAL, v); + + cg.writeEntry(KWIN_PLACEMENT, m_ui->placementCombo->itemData(m_ui->placementCombo->currentIndex()).toString()); + + cg.writeEntry(KWIN_HIDE_UTILITY, m_ui->hideUtilityWindowsForInactive->isChecked()); + cg.writeEntry(KWIN_INACTIVE_SKIP_TASKBAR, m_ui->inactiveTabsSkipTaskbar->isChecked()); + cg.writeEntry(KWIN_AUTOGROUP_SIMILAR, m_ui->autogroupSimilarWindows->isChecked()); + cg.writeEntry(KWIN_AUTOGROUP_FOREGROUND, m_ui->autogroupInForeground->isChecked()); + + if (standAlone) { + config->sync(); + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + + } + emit KCModule::changed(false); +} + +void KAdvancedConfig::defaults() +{ + setShadeHover(false); + setShadeHoverInterval(250); + m_ui->placementCombo->setCurrentIndex(0); // default to Smart + setHideUtilityWindowsForInactive(true); + setInactiveTabsSkipTaskbar(false); + setAutogroupSimilarWindows(false); + setAutogroupInForeground(true); + emit KCModule::changed(true); +} + + +void KAdvancedConfig::setHideUtilityWindowsForInactive(bool s) +{ + m_ui->hideUtilityWindowsForInactive->setChecked(s); +} + +void KAdvancedConfig::setInactiveTabsSkipTaskbar(bool s) +{ + m_ui->inactiveTabsSkipTaskbar->setChecked(s); +} + +void KAdvancedConfig::setAutogroupSimilarWindows(bool s) +{ + m_ui->autogroupSimilarWindows->setChecked(s); +} + +void KAdvancedConfig::setAutogroupInForeground(bool s) +{ + m_ui->autogroupInForeground->setChecked(s); +} + +KWinMovingConfigForm::KWinMovingConfigForm(QWidget* parent) + : QWidget(parent) +{ + setupUi(parent); +} + +KMovingConfig::~KMovingConfig() +{ + if (standAlone) + delete config; +} + +KMovingConfig::KMovingConfig(bool _standAlone, KConfig *_config, QWidget *parent) + : KCModule(parent), config(_config), standAlone(_standAlone) + , m_ui(new KWinMovingConfigForm(this)) +{ + // Any changes goes to slotChanged() + connect(m_ui->geometryTipOn, SIGNAL(clicked()), SLOT(changed())); + connect(m_ui->borderSnap, SIGNAL(valueChanged(int)), SLOT(changed())); + connect(m_ui->windowSnap, SIGNAL(valueChanged(int)), SLOT(changed())); + connect(m_ui->centerSnap, SIGNAL(valueChanged(int)), SLOT(changed())); + connect(m_ui->OverlapSnap, SIGNAL(clicked()), SLOT(changed())); + + load(); +} + +void KMovingConfig::setGeometryTip(bool showGeometryTip) +{ + m_ui->geometryTipOn->setChecked(showGeometryTip); +} + +bool KMovingConfig::getGeometryTip() +{ + return m_ui->geometryTipOn->isChecked(); +} + +void KMovingConfig::showEvent(QShowEvent *ev) +{ + if (!standAlone) { + QWidget::showEvent(ev); + return; + } + KCModule::showEvent(ev); +} + +void KMovingConfig::load(void) +{ + QString key; + + KConfigGroup cg(config, "Windows"); + + //KS 10Jan2003 - Geometry Tip during window move/resize + bool showGeomTip = cg.readEntry(KWIN_GEOMETRY, false); + setGeometryTip(showGeomTip); + + + int v; + + v = cg.readEntry(KWM_BRDR_SNAP_ZONE, KWM_BRDR_SNAP_ZONE_DEFAULT); + if (v > MAX_BRDR_SNAP) setBorderSnapZone(MAX_BRDR_SNAP); + else if (v < 0) setBorderSnapZone(0); + else setBorderSnapZone(v); + + v = cg.readEntry(KWM_WNDW_SNAP_ZONE, KWM_WNDW_SNAP_ZONE_DEFAULT); + if (v > MAX_WNDW_SNAP) setWindowSnapZone(MAX_WNDW_SNAP); + else if (v < 0) setWindowSnapZone(0); + else setWindowSnapZone(v); + + v = cg.readEntry(KWM_CNTR_SNAP_ZONE, KWM_CNTR_SNAP_ZONE_DEFAULT); + if (v > MAX_CNTR_SNAP) setCenterSnapZone(MAX_CNTR_SNAP); + else if (v < 0) setCenterSnapZone(0); + else setCenterSnapZone(v); + + m_ui->OverlapSnap->setChecked(cg.readEntry("SnapOnlyWhenOverlapping", false)); + emit KCModule::changed(false); +} + +void KMovingConfig::save(void) +{ + KConfigGroup cg(config, "Windows"); + cg.writeEntry(KWIN_GEOMETRY, getGeometryTip()); + + + cg.writeEntry(KWM_BRDR_SNAP_ZONE, getBorderSnapZone()); + cg.writeEntry(KWM_WNDW_SNAP_ZONE, getWindowSnapZone()); + cg.writeEntry(KWM_CNTR_SNAP_ZONE, getCenterSnapZone()); + cg.writeEntry("SnapOnlyWhenOverlapping", m_ui->OverlapSnap->isChecked()); + + const bool geometryTip = getGeometryTip(); + KConfigGroup(config, "Plugins").writeEntry("windowgeometryEnabled", geometryTip); + + if (standAlone) { + config->sync(); + // 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 (geometryTip) { + interface.loadEffect(KWin::BuiltInEffects::nameForEffect(KWin::BuiltInEffect::WindowGeometry)); + } else { + interface.unloadEffect(KWin::BuiltInEffects::nameForEffect(KWin::BuiltInEffect::WindowGeometry)); + } + emit KCModule::changed(false); +} + +void KMovingConfig::defaults() +{ + setGeometryTip(false); + + //copied from kcontrol/konq/kwindesktop, aleXXX + setWindowSnapZone(KWM_WNDW_SNAP_ZONE_DEFAULT); + setBorderSnapZone(KWM_BRDR_SNAP_ZONE_DEFAULT); + setCenterSnapZone(KWM_CNTR_SNAP_ZONE_DEFAULT); + m_ui->OverlapSnap->setChecked(false); + + emit KCModule::changed(true); +} + +int KMovingConfig::getBorderSnapZone() +{ + return m_ui->borderSnap->value(); +} + +void KMovingConfig::setBorderSnapZone(int pxls) +{ + m_ui->borderSnap->setValue(pxls); +} + +int KMovingConfig::getWindowSnapZone() +{ + return m_ui->windowSnap->value(); +} + +void KMovingConfig::setWindowSnapZone(int pxls) +{ + m_ui->windowSnap->setValue(pxls); +} + +int KMovingConfig::getCenterSnapZone() +{ + return m_ui->centerSnap->value(); +} + +void KMovingConfig::setCenterSnapZone(int pxls) +{ + m_ui->centerSnap->setValue(pxls); +} + diff --git a/kcmkwin/kwinoptions/windows.h b/kcmkwin/kwinoptions/windows.h new file mode 100644 index 0000000..959afac --- /dev/null +++ b/kcmkwin/kwinoptions/windows.h @@ -0,0 +1,191 @@ +/* + * windows.h + * + * Copyright (c) 1997 Patrick Dowler dowler@morgul.fsh.uvic.ca + * Copyright (c) 2001 Waldo Bastian bastian@kde.org + * + * 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. + */ + +#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 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, KConfig *_config, QWidget *parent); + ~KFocusConfig(); + + void load(); + void save(); + void defaults(); + +protected: + void showEvent(QShowEvent *ev); + +private Q_SLOTS: + void setDelayFocusEnabled(); + void focusPolicyChanged(); + void autoRaiseOnTog(bool);//CT 23Oct1998 + void delayFocusOnTog(bool); + void updateActiveMouseScreen(); + void updateMultiScreen(); + void changed() { + emit KCModule::changed(true); + } + + +private: + + int getFocus(void); + int getAutoRaiseInterval(void); + int getDelayFocusInterval(void); + + void setFocus(int); + void setAutoRaiseInterval(int); + void setAutoRaise(bool); + void setDelayFocusInterval(int); + void setClickRaise(bool); + void setSeparateScreenFocus(bool); + void setActiveMouseScreen(bool); + + void setFocusStealing(int); + + KConfig *config; + bool standAlone; + + KWinFocusConfigForm *m_ui; +}; + +class KMovingConfig : public KCModule +{ + Q_OBJECT +public: + KMovingConfig(bool _standAlone, KConfig *config, QWidget *parent); + ~KMovingConfig(); + + void load(); + void save(); + void defaults(); + +protected: + void showEvent(QShowEvent *ev); + +private Q_SLOTS: + void changed() { + emit KCModule::changed(true); + } + +private: + bool getGeometryTip(void); //KS + + void setGeometryTip(bool); //KS + + KConfig *config; + bool standAlone; + KWinMovingConfigForm *m_ui; + + int getBorderSnapZone(); + void setBorderSnapZone(int); + int getWindowSnapZone(); + void setWindowSnapZone(int); + int getCenterSnapZone(); + void setCenterSnapZone(int); + +}; + +class KAdvancedConfig : public KCModule +{ + Q_OBJECT +public: + KAdvancedConfig(bool _standAlone, KConfig *config, QWidget *parent); + ~KAdvancedConfig(); + + void load(); + void save(); + void defaults(); + +protected: + void showEvent(QShowEvent *ev); + +private Q_SLOTS: + void shadeHoverChanged(bool); + + void changed() { + emit KCModule::changed(true); + } + +private: + + int getShadeHoverInterval(void); + void setShadeHover(bool); + void setShadeHoverInterval(int); + + KConfig *config; + bool standAlone; + KWinAdvancedConfigForm *m_ui; + + void setHideUtilityWindowsForInactive(bool); + void setInactiveTabsSkipTaskbar(bool); + void setAutogroupSimilarWindows(bool); + void setAutogroupInForeground(bool); +}; + +#endif // KKWMWINDOWS_H diff --git a/kcmkwin/kwinrules/CMakeLists.txt b/kcmkwin/kwinrules/CMakeLists.txt new file mode 100644 index 0000000..b53f7b5 --- /dev/null +++ b/kcmkwin/kwinrules/CMakeLists.txt @@ -0,0 +1,62 @@ +# KI18N Translation Domain for this library +add_definitions(-DTRANSLATION_DOMAIN=\"kcmkwinrules\") +add_definitions(-DKCMRULES) +########### next target ############### + +include_directories(../../) +set (kwinrules_MOC_HDRS yesnobox.h ../../client_machine.h ../../cursor.h ../../plugins/platforms/x11/standalone/x11cursor.h) +qt5_wrap_cpp(kwinrules_MOC_SRCS ${kwinrules_MOC_HDRS}) +set(kwinrules_SRCS ruleswidget.cpp ruleslist.cpp kwinsrc.cpp detectwidget.cpp ${kwinrules_MOC_SRCS}) + +ki18n_wrap_ui(kwinrules_SRCS ruleslist.ui detectwidget.ui editshortcut.ui ruleswidgetbase.ui) + +set(kwin_rules_dialog_KDEINIT_SRCS main.cpp ${kwinrules_SRCS}) + +kf5_add_kdeinit_executable( kwin_rules_dialog ${kwin_rules_dialog_KDEINIT_SRCS}) + +set(kwin_kcm_rules_XCB_LIBS + XCB::XCB + XCB::XFIXES + XCB::CURSOR +) + +set(kcm_libs + Qt5::Concurrent + Qt5::X11Extras + KF5::Completion + KF5::ConfigWidgets + KF5::I18n + KF5::Service + KF5::WindowSystem + KF5::XmlGui +) + +if(KWIN_BUILD_ACTIVITIES) + set(kcm_libs ${kcm_libs} KF5::Activities) +endif() + +target_link_libraries(kdeinit_kwin_rules_dialog ${kcm_libs} ${kwin_kcm_rules_XCB_LIBS}) + +install(TARGETS kdeinit_kwin_rules_dialog ${INSTALL_TARGETS_DEFAULT_ARGS} ) +install(TARGETS kwin_rules_dialog DESTINATION ${LIBEXEC_INSTALL_DIR} ) + +########### next target ############### + +set(kcm_kwinrules_PART_SRCS kcm.cpp ${kwinrules_SRCS}) + + +add_library(kcm_kwinrules MODULE ${kcm_kwinrules_PART_SRCS}) + +target_link_libraries(kcm_kwinrules ${kcm_libs} ${kwin_kcm_rules_XCB_LIBS}) + +install(TARGETS kcm_kwinrules DESTINATION ${PLUGIN_INSTALL_DIR} ) + + +########### next target ############### + + +########### install files ############### + +install( FILES kwinrules.desktop DESTINATION ${SERVICES_INSTALL_DIR} ) + + diff --git a/kcmkwin/kwinrules/Messages.sh b/kcmkwin/kwinrules/Messages.sh new file mode 100644 index 0000000..470c73d --- /dev/null +++ b/kcmkwin/kwinrules/Messages.sh @@ -0,0 +1,4 @@ +#! /usr/bin/env bash +$EXTRACTRC *.ui >> rc.cpp || exit 11 +$XGETTEXT *.cpp *.h -o $podir/kcmkwinrules.pot +rm -f rc.cpp diff --git a/kcmkwin/kwinrules/detectwidget.cpp b/kcmkwin/kwinrules/detectwidget.cpp new file mode 100644 index 0000000..db00184 --- /dev/null +++ b/kcmkwin/kwinrules/detectwidget.cpp @@ -0,0 +1,176 @@ +/* + * Copyright (c) 2004 Lubos Lunak + * + * 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. + */ + +#include "detectwidget.h" +#include "../../plugins/platforms/x11/standalone/x11cursor.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +Q_DECLARE_METATYPE(NET::WindowType) + +namespace KWin +{ + +DetectWidget::DetectWidget(QWidget* parent) + : QWidget(parent) +{ + setupUi(this); +} + +DetectDialog::DetectDialog(QWidget* parent, const char* name) + : QDialog(parent) +{ + setObjectName(name); + setModal(true); + setLayout(new QVBoxLayout); + + widget = new DetectWidget(this); + layout()->addWidget(widget); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok|QDialogButtonBox::Cancel, this); + layout()->addWidget(buttons); + + connect(buttons, SIGNAL(accepted()), SLOT(accept())); + connect(buttons, SIGNAL(rejected()), SLOT(reject())); +} + +void DetectDialog::detect(int secs) +{ + QTimer::singleShot(secs*1000, this, SLOT(selectWindow())); +} + +void DetectDialog::executeDialog() +{ + static const char* const types[] = { + I18N_NOOP("Normal Window"), + I18N_NOOP("Desktop"), + I18N_NOOP("Dock (panel)"), + I18N_NOOP("Toolbar"), + I18N_NOOP("Torn-Off Menu"), + I18N_NOOP("Dialog Window"), + I18N_NOOP("Override Type"), + I18N_NOOP("Standalone Menubar"), + I18N_NOOP("Utility Window"), + I18N_NOOP("Splash Screen") + }; + widget->class_label->setText(wmclass_class + QLatin1String(" (") + wmclass_name + ' ' + wmclass_class + ')'); + widget->role_label->setText(role); + widget->match_role->setEnabled(!role.isEmpty()); + if (type == NET::Unknown) + widget->type_label->setText(i18n("Unknown - will be treated as Normal Window")); + else + widget->type_label->setText(i18n(types[ type ])); + widget->title_label->setText(title); + widget->machine_label->setText(machine); + widget->adjustSize(); + adjustSize(); + if (width() < 4*height()/3) + resize(4*height()/3, height()); + emit detectionDone(exec() == QDialog::Accepted); +} + +QByteArray DetectDialog::selectedClass() const +{ + if (widget->match_whole_class->isChecked()) + return wmclass_name + ' ' + wmclass_class; + return wmclass_class; +} + +bool DetectDialog::selectedWholeClass() const +{ + return widget->match_whole_class->isChecked(); +} + +QByteArray DetectDialog::selectedRole() const +{ + if (widget->match_role->isChecked()) + return role; + return ""; +} + +QString DetectDialog::selectedTitle() const +{ + return title; +} + +Rules::StringMatch DetectDialog::titleMatch() const +{ + return widget->match_title->isChecked() ? Rules::ExactMatch : Rules::UnimportantMatch; +} + +bool DetectDialog::selectedWholeApp() const +{ + return !widget->match_type->isChecked(); +} + +NET::WindowType DetectDialog::selectedType() const +{ + return type; +} + +QByteArray DetectDialog::selectedMachine() const +{ + return machine; +} + +void DetectDialog::selectWindow() +{ + 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()) { + emit detectionDone(false); + return; + } + m_windowInfo = reply.value(); + wmclass_class = m_windowInfo.value("resourceClass").toByteArray(); + wmclass_name = m_windowInfo.value("resourceName").toByteArray(); + role = m_windowInfo.value("role").toByteArray(); + type = m_windowInfo.value("type").value(); + title = m_windowInfo.value("caption").toString(); + machine = m_windowInfo.value("clientMachine").toByteArray(); + executeDialog(); + } + ); +} + +} // namespace + diff --git a/kcmkwin/kwinrules/detectwidget.h b/kcmkwin/kwinrules/detectwidget.h new file mode 100644 index 0000000..c0a208f --- /dev/null +++ b/kcmkwin/kwinrules/detectwidget.h @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2004 Lubos Lunak + * + * 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. + */ + + +#ifndef __DETECTWIDGET_H__ +#define __DETECTWIDGET_H__ + +#include +#include + +#include "../../rules.h" +//Added by qt3to4: +#include +#include + +#include "ui_detectwidget.h" + +namespace KWin +{ + +class DetectWidget + : public QWidget, public Ui_DetectWidget +{ + Q_OBJECT +public: + explicit DetectWidget(QWidget* parent = nullptr); +}; + +class DetectDialog + : public QDialog +{ + Q_OBJECT +public: + explicit DetectDialog(QWidget* parent = nullptr, const char* name = nullptr); + void detect(int secs = 0); + QByteArray selectedClass() const; + bool selectedWholeClass() const; + QByteArray selectedRole() const; + bool selectedWholeApp() const; + NET::WindowType selectedType() const; + QString selectedTitle() const; + Rules::StringMatch titleMatch() const; + QByteArray selectedMachine() const; + + const QVariantMap &windowInfo() const { + return m_windowInfo; + } + +Q_SIGNALS: + void detectionDone(bool); +private Q_SLOTS: + void selectWindow(); +private: + void executeDialog(); + QByteArray wmclass_class; + QByteArray wmclass_name; + QByteArray role; + NET::WindowType type; + QString title; + QByteArray extrarole; + QByteArray machine; + DetectWidget* widget; + QVariantMap m_windowInfo; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwinrules/detectwidget.ui b/kcmkwin/kwinrules/detectwidget.ui new file mode 100644 index 0000000..9fd3aa7 --- /dev/null +++ b/kcmkwin/kwinrules/detectwidget.ui @@ -0,0 +1,229 @@ + + + KWin::DetectWidget + + + + 0 + 0 + 450 + 300 + + + + + + + Information About Selected Window + + + true + + + + 0 + + + + + Class: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + + + + + + true + + + + + + + false + + + + + + + Role: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + + + + + + true + + + + + + + false + + + + + + + Type: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + + + + + + true + + + + + + + false + + + + + + + Title: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + + + + + + true + + + + + + + false + + + + + + + Machine: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + + + + + + true + + + + + + + false + + + + + + + + + + Match by primary class name and + + + true + + + + + + Secondary class name (resulting in term in brackets) + + + + + + + Window role (can be used to select windows by function) + + + + + + + Window type (eg. all dialogs, but not the main windows) + + + + + + + Window title (very specific, can fail due to content changes or translation) + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 0 + + + + + + + + + diff --git a/kcmkwin/kwinrules/editshortcut.ui b/kcmkwin/kwinrules/editshortcut.ui new file mode 100644 index 0000000..bfc1f93 --- /dev/null +++ b/kcmkwin/kwinrules/editshortcut.ui @@ -0,0 +1,161 @@ + + EditShortcut + + + + 0 + 0 + 1194 + 447 + + + + + + + A single shortcut can be easily assigned or cleared using the two buttons. Only shortcuts with modifiers can be used.<p> +It is possible to have several possible shortcuts, and the first available shortcut will be used. The shortcuts are specified using shortcut sets separated by " - ". One set is specified as <i>base</i>+(<i>list</i>), where base are modifiers and list is a list of keys.<br> +For example "<b>Shift+Alt+(123) Shift+Ctrl+(ABC)</b>" will first try <b>Shift+Alt+1</b>, then others with <b>Shift+Ctrl+C</b> as the last one. + + + Qt::RichText + + + true + + + + + + + QFrame::HLine + + + QFrame::Sunken + + + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + &Single Shortcut + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + C&lear + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + + + QFrame::HLine + + + QFrame::Sunken + + + + + + + + KLineEdit + QWidget +
klineedit.h
+
+
+ + + + pushButton1 + clicked() + EditShortcut + editShortcut() + + + 20 + 20 + + + 20 + 20 + + + + + pushButton2 + clicked() + EditShortcut + clearShortcut() + + + 20 + 20 + + + 20 + 20 + + + + +
diff --git a/kcmkwin/kwinrules/kcm.cpp b/kcmkwin/kwinrules/kcm.cpp new file mode 100644 index 0000000..2a0ef2c --- /dev/null +++ b/kcmkwin/kwinrules/kcm.cpp @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2004 Lubos Lunak + * + * 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. + */ + +#include "kcm.h" + +#include +//Added by qt3to4: +#include +#include +#include +#include +#include + +#include "ruleslist.h" +#include +#include + +K_PLUGIN_FACTORY(KCMRulesFactory, + registerPlugin(); + ) + +namespace KWin +{ + +KCMRules::KCMRules(QWidget *parent, const QVariantList &) + : KCModule(parent) + , config("kwinrulesrc") +{ + QVBoxLayout *layout = new QVBoxLayout(this); + layout->setMargin(0); + + widget = new KCMRulesList(this); + layout->addWidget(widget); + connect(widget, SIGNAL(changed(bool)), SLOT(moduleChanged(bool))); + KAboutData *about = new KAboutData(QStringLiteral("kcmkwinrules"), + i18n("Window-Specific Settings Configuration Module"), + QString(), QString(), KAboutLicense::GPL, i18n("(c) 2004 KWin and KControl Authors")); + about->addAuthor(i18n("Lubos Lunak"), QString(), "l.lunak@kde.org"); + setAboutData(about); +} + +void KCMRules::load() +{ + config.reparseConfiguration(); + widget->load(); + emit KCModule::changed(false); +} + +void KCMRules::save() +{ + widget->save(); + emit KCModule::changed(false); + // Send signal to kwin + config.sync(); + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + +} + +void KCMRules::defaults() +{ + widget->defaults(); +} + +QString KCMRules::quickHelp() const +{ + return 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.

"); +} + +void KCMRules::moduleChanged(bool state) +{ + emit KCModule::changed(state); +} + +} + +// i18n freeze :-/ +#if 0 +I18N_NOOP("Remember settings separately for every window") +I18N_NOOP("Show internal settings for remembering") +I18N_NOOP("Internal setting for remembering") +#endif + + +#include "kcm.moc" diff --git a/kcmkwin/kwinrules/kcm.h b/kcmkwin/kwinrules/kcm.h new file mode 100644 index 0000000..f804ee9 --- /dev/null +++ b/kcmkwin/kwinrules/kcm.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2004 Lubos Lunak + * + * 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. + */ + + +#ifndef __KCM_H__ +#define __KCM_H__ + +#include +#include + +class KConfig; + +namespace KWin +{ + +class KCMRulesList; + +class KCMRules + : public KCModule +{ + Q_OBJECT +public: + KCMRules(QWidget *parent, const QVariantList &args); + virtual void load(); + virtual void save(); + virtual void defaults(); + virtual QString quickHelp() const; +protected Q_SLOTS: + void moduleChanged(bool state); +private: + KCMRulesList* widget; + KConfig config; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwinrules/kwinrules.desktop b/kcmkwin/kwinrules/kwinrules.desktop new file mode 100644 index 0000000..48a81c4 --- /dev/null +++ b/kcmkwin/kwinrules/kwinrules.desktop @@ -0,0 +1,162 @@ +[Desktop Entry] +Exec=kcmshell5 kwinrules +Icon=preferences-system-windows-actions +Type=Service +X-KDE-ServiceTypes=KCModule +X-DocPath=kcontrol/windowspecific/index.html + +X-KDE-Library=kcm_kwinrules +X-KDE-ParentApp=kcontrol + +X-KDE-System-Settings-Parent-Category=windowmanagement +X-KDE-Weight=120 + +Name=Window Rules +Name[ar]=قواعد النوافذ +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 la ventana +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]=Window Rules +Name[is]=Gluggahegðunarreglur +Name[it]=Regole delle finestre +Name[ja]=ウィンドウルール +Name[kk]=Терезе тәртібі +Name[km]=ក្បួន​បង្អួច +Name[kn]=ವಿಂಡೋ ನಿಯಮಗಳು +Name[ko]=창 규칙 +Name[lt]=Lango 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 fereastră +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[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 Jendela Individu +Comment[it]=Comportamento della singola finestra +Comment[ja]=個別のウィンドウの挙動 +Comment[ko]=개별 창 동작 +Comment[lt]=Individuali lango 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[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[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 la ventana,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 jendela,jendela,spesifik,sekeliling,ingat,aturan +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[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[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/kwinsrc.cpp b/kcmkwin/kwinrules/kwinsrc.cpp new file mode 100644 index 0000000..4f48db8 --- /dev/null +++ b/kcmkwin/kwinrules/kwinsrc.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2004 Lubos Lunak + * + * 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. + */ + +// Include some code from kwin core in order to avoid +// double implementation. + +#include "ruleslist.h" +#include "../../cursor.cpp" +#include "../../plugins/platforms/x11/standalone/x11cursor.cpp" +#include "../../rules.cpp" +#include "../../placement.cpp" +#include "../../options.cpp" +#include "../../utils.cpp" +#include "../../client_machine.cpp" + +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..0103bfc --- /dev/null +++ b/kcmkwin/kwinrules/main.cpp @@ -0,0 +1,275 @@ +/* + * Copyright (c) 2004 Lubos Lunak + * + * 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. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ruleswidget.h" +#include "../../rules.h" +#include "../../client_machine.h" +#include + +namespace KWin +{ + +static void loadRules(QList< Rules* >& rules) +{ + KConfig _cfg("kwinrulesrc"); + KConfigGroup cfg(&_cfg, "General"); + int count = cfg.readEntry("count", 0); + for (int i = 1; + i <= count; + ++i) { + cfg = KConfigGroup(&_cfg, QString::number(i)); + Rules* rule = new Rules(cfg); + rules.append(rule); + } +} + +static void saveRules(const QList< Rules* >& rules) +{ + KConfig cfg("kwinrulesrc"); + QStringList groups = cfg.groupList(); + for (QStringList::ConstIterator it = groups.constBegin(); + it != groups.constEnd(); + ++it) + cfg.deleteGroup(*it); + cfg.group("General").writeEntry("count", rules.count()); + int i = 1; + for (QList< Rules* >::ConstIterator it = rules.constBegin(); + it != rules.constEnd(); + ++it) { + KConfigGroup cg(&cfg, QString::number(i)); + (*it)->write(cg); + ++i; + } +} + +static Rules* findRule(const QList< Rules* >& rules, Window wid, bool whole_app) +{ + // ClientMachine::resolve calls NETWinInfo::update() which requires properties + // bug #348472 ./. bug #346748 + if (QX11Info::isPlatformX11()) { + qApp->setProperty("x11Connection", QVariant::fromValue(QX11Info::connection())); + qApp->setProperty("x11RootWindow", QVariant::fromValue(QX11Info::appRootWindow())); + } + KWindowInfo info = KWindowInfo(wid, + NET::WMName | NET::WMWindowType, + NET::WM2WindowClass | NET::WM2WindowRole | NET::WM2ClientMachine); + if (!info.valid()) // shouldn't really happen + return nullptr; + ClientMachine clientMachine; + clientMachine.resolve(info.win(), info.groupLeader()); + QByteArray wmclass_class = info.windowClassClass().toLower(); + QByteArray wmclass_name = info.windowClassName().toLower(); + QByteArray role = info.windowRole().toLower(); + NET::WindowType type = info.windowType(NET::NormalMask | NET::DesktopMask | NET::DockMask + | NET::ToolbarMask | NET::MenuMask | NET::DialogMask | NET::OverrideMask | NET::TopMenuMask + | NET::UtilityMask | NET::SplashMask); + QString title = info.name(); + QByteArray machine = clientMachine.hostName(); + Rules* best_match = nullptr; + int match_quality = 0; + for (QList< Rules* >::ConstIterator it = rules.constBegin(); + it != rules.constEnd(); + ++it) { + // try to find an exact match, i.e. not a generic rule + Rules* rule = *it; + 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, clientMachine.isLocal())) + 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 int edit(Window wid, bool whole_app) +{ + QList< Rules* > rules; + loadRules(rules); + Rules* orig_rule = findRule(rules, wid, 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, wid, 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; + } + saveRules(rules); + // Send signal to all kwin instances + QDBusMessage message = + QDBusMessage::createSignal("/KWin", "org.kde.KWin", "reloadConfig"); + QDBusConnection::sessionBus().send(message); + return 0; +} + +} // namespace + +extern "C" +KWIN_EXPORT int kdemain(int argc, char* argv[]) +{ + qputenv("QT_QPA_PLATFORM", "xcb"); + QApplication app(argc, argv); + app.setApplicationDisplayName(i18n("KWin")); + app.setApplicationName("kwin_rules_dialog"); + app.setApplicationVersion("1.0"); + bool whole_app = false; + bool id_ok = false; + Window id = None; + { + QCommandLineParser parser; + parser.setApplicationDescription(i18n("KWin helper utility")); + parser.addOption(QCommandLineOption("wid", i18n("WId of the window for special window settings."), "wid")); + parser.addOption(QCommandLineOption("whole-app", i18n("Whether the settings should affect all windows of the application."))); + parser.process(app); + + id = parser.value("wid").toULongLong(&id_ok); + whole_app = parser.isSet("whole-app"); + } + + if (!id_ok || id == None) { + printf("%s\n", qPrintable(i18n("This helper utility is not supposed to be called directly."))); + return 1; + } + return KWin::edit(id, whole_app); +} diff --git a/kcmkwin/kwinrules/ruleslist.cpp b/kcmkwin/kwinrules/ruleslist.cpp new file mode 100644 index 0000000..4957113 --- /dev/null +++ b/kcmkwin/kwinrules/ruleslist.cpp @@ -0,0 +1,264 @@ +/* + * Copyright (c) 2004 Lubos Lunak + * + * 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. + */ + +#include "ruleslist.h" + +#include +#include +#include +#include + +#include "ruleswidget.h" + +namespace KWin +{ + +KCMRulesList::KCMRulesList(QWidget* parent) + : QWidget(parent) +{ + setupUi(this); + // connect both current/selected, so that current==selected (stupid QListBox :( ) + connect(rules_listbox, SIGNAL(itemChanged(QListWidgetItem*)), + SLOT(activeChanged())); + connect(rules_listbox, SIGNAL(itemSelectionChanged()), + SLOT(activeChanged())); + connect(new_button, SIGNAL(clicked()), + SLOT(newClicked())); + connect(modify_button, SIGNAL(clicked()), + SLOT(modifyClicked())); + connect(delete_button, SIGNAL(clicked()), + SLOT(deleteClicked())); + connect(moveup_button, SIGNAL(clicked()), + SLOT(moveupClicked())); + connect(movedown_button, SIGNAL(clicked()), + SLOT(movedownClicked())); + connect(export_button, SIGNAL(clicked()), + SLOT(exportClicked())); + connect(import_button, SIGNAL(clicked()), + SLOT(importClicked())); + connect(rules_listbox, SIGNAL(itemDoubleClicked(QListWidgetItem*)), + SLOT(modifyClicked())); + load(); +} + +KCMRulesList::~KCMRulesList() +{ + for (QVector< Rules* >::Iterator it = rules.begin(); + it != rules.end(); + ++it) + delete *it; + rules.clear(); +} + +void KCMRulesList::activeChanged() +{ + QListWidgetItem *item = rules_listbox->currentItem(); + int itemRow = rules_listbox->row(item); + + if (item != nullptr) // make current==selected + rules_listbox->setCurrentItem(item, QItemSelectionModel::ClearAndSelect); + modify_button->setEnabled(item != nullptr); + delete_button->setEnabled(item != nullptr); + export_button->setEnabled(item != nullptr); + moveup_button->setEnabled(item != nullptr && itemRow > 0); + movedown_button->setEnabled(item != nullptr && itemRow < (rules_listbox->count() - 1)); +} + +void KCMRulesList::newClicked() +{ + RulesDialog dlg(this); + Rules* rule = dlg.edit(nullptr, 0, false); + if (rule == nullptr) + return; + int pos = rules_listbox->currentRow() + 1; + rules_listbox->insertItem(pos , rule->description); + rules_listbox->setCurrentRow(pos, QItemSelectionModel::ClearAndSelect); + rules.insert(rules.begin() + pos, rule); + emit changed(true); +} + +void KCMRulesList::modifyClicked() +{ + int pos = rules_listbox->currentRow(); + if (pos == -1) + return; + RulesDialog dlg(this); + Rules* rule = dlg.edit(rules[ pos ], 0, false); + if (rule == rules[ pos ]) + return; + delete rules[ pos ]; + rules[ pos ] = rule; + rules_listbox->item(pos)->setText(rule->description); + emit changed(true); +} + +void KCMRulesList::deleteClicked() +{ + int pos = rules_listbox->currentRow(); + assert(pos != -1); + delete rules_listbox->takeItem(pos); + rules.erase(rules.begin() + pos); + emit changed(true); +} + +void KCMRulesList::moveupClicked() +{ + int pos = rules_listbox->currentRow(); + assert(pos != -1); + if (pos > 0) { + QListWidgetItem * item = rules_listbox->takeItem(pos); + rules_listbox->insertItem(pos - 1 , item); + rules_listbox->setCurrentItem(item, QItemSelectionModel::ClearAndSelect); + Rules* rule = rules[ pos ]; + rules[ pos ] = rules[ pos - 1 ]; + rules[ pos - 1 ] = rule; + } + emit changed(true); +} + +void KCMRulesList::movedownClicked() +{ + int pos = rules_listbox->currentRow(); + assert(pos != -1); + if (pos < int(rules_listbox->count()) - 1) { + QListWidgetItem * item = rules_listbox->takeItem(pos); + rules_listbox->insertItem(pos + 1 , item); + rules_listbox->setCurrentItem(item, QItemSelectionModel::ClearAndSelect); + Rules* rule = rules[ pos ]; + rules[ pos ] = rules[ pos + 1 ]; + rules[ pos + 1 ] = rule; + } + emit changed(true); +} + +void KCMRulesList::exportClicked() +{ + int pos = rules_listbox->currentRow(); + assert(pos != -1); + QString path = QFileDialog::getSaveFileName(this, i18n("Export Rules"), QDir::home().absolutePath(), + i18n("KWin Rules (*.kwinrule)")); + if (path.isEmpty()) + return; + KConfig config(path, KConfig::SimpleConfig); + KConfigGroup group(&config, rules[pos]->description); + group.deleteGroup(); + rules[pos]->write(group); +} + +void KCMRulesList::importClicked() +{ + QString path = QFileDialog::getOpenFileName(this, i18n("Import Rules"), QDir::home().absolutePath(), + i18n("KWin Rules (*.kwinrule)")); + if (path.isEmpty()) + return; + KConfig config(path, KConfig::SimpleConfig); + QStringList groups = config.groupList(); + if (groups.isEmpty()) + return; + + int pos = qMax(0, rules_listbox->currentRow()); + foreach (const QString &group, groups) { + KConfigGroup grp(&config, group); + const bool remove = grp.readEntry("DeleteRule", false); + Rules* new_rule = new Rules(grp); + + // try to replace existing rule first + for (int i = 0; i < rules.count(); ++i) { + if (rules[i]->description == new_rule->description) { + delete rules[i]; + if (remove) { + rules.remove(i); + delete rules_listbox->takeItem(i); + delete new_rule; + pos = qMax(0, rules_listbox->currentRow()); // might have changed! + } + else + rules[i] = new_rule; + new_rule = nullptr; + break; + } + } + + // don't add "to be deleted" if not present + if (remove) { + delete new_rule; + new_rule = nullptr; + } + + // plain insertion + if (new_rule) { + rules.insert(pos, new_rule); + rules_listbox->insertItem(pos++, new_rule->description); + } + } + emit changed(true); +} + +void KCMRulesList::load() +{ + rules_listbox->clear(); + for (QVector< Rules* >::Iterator it = rules.begin(); + it != rules.end(); + ++it) + delete *it; + rules.clear(); + KConfig _cfg("kwinrulesrc"); + KConfigGroup cfg(&_cfg, "General"); + int count = cfg.readEntry("count", 0); + rules.reserve(count); + for (int i = 1; + i <= count; + ++i) { + cfg = KConfigGroup(&_cfg, QString::number(i)); + Rules* rule = new Rules(cfg); + rules.append(rule); + rules_listbox->addItem(rule->description); + } + if (rules.count() > 0) + rules_listbox->setCurrentItem(rules_listbox->item(0)); + else + rules_listbox->setCurrentItem(nullptr); + activeChanged(); +} + +void KCMRulesList::save() +{ + KConfig cfg(QLatin1String("kwinrulesrc")); + QStringList groups = cfg.groupList(); + for (QStringList::ConstIterator it = groups.constBegin(); + it != groups.constEnd(); + ++it) + cfg.deleteGroup(*it); + cfg.group("General").writeEntry("count", rules.count()); + int i = 1; + for (QVector< Rules* >::ConstIterator it = rules.constBegin(); + it != rules.constEnd(); + ++it) { + KConfigGroup cg(&cfg, QString::number(i)); + (*it)->write(cg); + ++i; + } +} + +void KCMRulesList::defaults() +{ + load(); +} + +} // namespace + diff --git a/kcmkwin/kwinrules/ruleslist.h b/kcmkwin/kwinrules/ruleslist.h new file mode 100644 index 0000000..eb9a1ad --- /dev/null +++ b/kcmkwin/kwinrules/ruleslist.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2004 Lubos Lunak + * + * 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. + */ + + +#ifndef __RULESLIST_H__ +#define __RULESLIST_H__ + +#include "../../rules.h" + +#include "ui_ruleslist.h" + +namespace KWin +{ + +class KCMRulesList + : public QWidget, Ui_KCMRulesList +{ + Q_OBJECT +public: + explicit KCMRulesList(QWidget* parent = nullptr); + virtual ~KCMRulesList(); + void load(); + void save(); + void defaults(); +Q_SIGNALS: + void changed(bool); +private Q_SLOTS: + void newClicked(); + void modifyClicked(); + void deleteClicked(); + void moveupClicked(); + void movedownClicked(); + void exportClicked(); + void importClicked(); + void activeChanged(); +private: + QVector< Rules* > rules; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwinrules/ruleslist.ui b/kcmkwin/kwinrules/ruleslist.ui new file mode 100644 index 0000000..a0f45c1 --- /dev/null +++ b/kcmkwin/kwinrules/ruleslist.ui @@ -0,0 +1,122 @@ + + + KWin::KCMRulesList + + + + 0 + 0 + 600 + 480 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + &New... + + + + + + + &Modify... + + + + + + + Delete + + + + + + + + + + Move &Up + + + + + + + Move &Down + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 294 + + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + + + + &Import + + + + + + + &Export + + + + + + + Qt::Horizontal + + + + + + + + diff --git a/kcmkwin/kwinrules/ruleswidget.cpp b/kcmkwin/kwinrules/ruleswidget.cpp new file mode 100644 index 0000000..3037442 --- /dev/null +++ b/kcmkwin/kwinrules/ruleswidget.cpp @@ -0,0 +1,1011 @@ +/* + * Copyright (c) 2004 Lubos Lunak + * + * 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. + */ + +#include "ruleswidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef KWIN_BUILD_ACTIVITIES +#include +#endif + +#include +#include +#include +#include + +#include "../../rules.h" + +#include "detectwidget.h" + +Q_DECLARE_METATYPE(NET::WindowType) + +namespace KWin +{ + +#define SETUP( var, type ) \ + connect( enable_##var, SIGNAL(toggled(bool)), rule_##var, SLOT(setEnabled(bool))); \ + connect( enable_##var, SIGNAL(toggled(bool)), this, SLOT(updateEnable##var())); \ + connect( rule_##var, SIGNAL(activated(int)), this, SLOT(updateEnable##var())); \ + enable_##var->setWhatsThis( enableDesc ); \ + rule_##var->setWhatsThis( type##RuleDesc ); + +RulesWidget::RulesWidget(QWidget* parent) + : detect_dlg(nullptr) +{ + Q_UNUSED(parent); + setupUi(this); + QRegularExpressionValidator* validator = new QRegularExpressionValidator(QRegularExpression("[0-9\\-+,xX:]*"), this); + maxsize->setValidator(validator); + minsize->setValidator(validator); + position->setValidator(validator); + Ui::RulesWidgetBase::size->setValidator(validator); + + QString enableDesc = + i18n("Enable this checkbox to alter this window property for the specified window(s)."); + QString setRuleDesc = + i18n("Specify how the window property should be affected:
    " + "
  • Do Not Affect: The window property will not be affected and therefore" + " the default handling for it will be used. Specifying this will block more generic" + " window settings from taking effect.
  • " + "
  • Apply Initially: The window property will be only set to the given value" + " after the window is created. No further changes will be affected.
  • " + "
  • Remember: The value of the window property will be remembered and every" + " time the window is created, the last remembered value will be applied.
  • " + "
  • Force: The window property will be always forced to the given value.
  • " + "
  • Apply Now: The window property will be set to the given value immediately" + " and will not be affected later (this action will be deleted afterwards).
  • " + "
  • Force temporarily: The window property will be forced to the given value" + " until it is hidden (this action will be deleted after the window is hidden).
  • " + "
"); + QString forceRuleDesc = + i18n("Specify how the window property should be affected:
    " + "
  • Do Not Affect: The window property will not be affected and therefore" + " the default handling for it will be used. Specifying this will block more generic" + " window settings from taking effect.
  • " + "
  • Force: The window property will be always forced to the given value.
  • " + "
  • Force temporarily: The window property will be forced to the given value" + " until it is hidden (this action will be deleted after the window is hidden).
  • " + "
"); + // window tabs have enable signals done in designer + // geometry tab + SETUP(position, set); + SETUP(size, set); + SETUP(desktop, set); + SETUP(screen, set); +#ifdef KWIN_BUILD_ACTIVITIES + SETUP(activity, set); +#endif + SETUP(maximizehoriz, set); + SETUP(maximizevert, set); + SETUP(minimize, set); + SETUP(shade, set); + SETUP(fullscreen, set); + SETUP(placement, force); + // preferences tab + SETUP(above, set); + SETUP(below, set); + SETUP(noborder, set); + SETUP(decocolor, force); + SETUP(skiptaskbar, set); + SETUP(skippager, set); + SETUP(skipswitcher, set); + SETUP(acceptfocus, force); + SETUP(closeable, force); + SETUP(autogroup, force); + SETUP(autogroupfg, force); + SETUP(autogroupid, force); + SETUP(opacityactive, force); + SETUP(opacityinactive, force); + SETUP(shortcut, force); + // workarounds tab + SETUP(fsplevel, force); + SETUP(fpplevel, force); + SETUP(type, force); + SETUP(desktopfile, set); + SETUP(ignoregeometry, set); + SETUP(minsize, force); + SETUP(maxsize, force); + SETUP(strictgeometry, force); + SETUP(disableglobalshortcuts, force); + SETUP(blockcompositing, force); + + connect (shortcut_edit, SIGNAL(clicked()), SLOT(shortcutEditClicked())); + + edit_reg_wmclass->hide(); + edit_reg_role->hide(); + edit_reg_title->hide(); + edit_reg_machine->hide(); + +#ifndef KWIN_BUILD_ACTIVITIES + rule_activity->hide(); + enable_activity->hide(); + activity->hide(); +#endif + int i; + for (i = 1; + i <= KWindowSystem::numberOfDesktops(); + ++i) + desktop->addItem(QString::number(i).rightJustified(2) + ':' + KWindowSystem::desktopName(i)); + desktop->addItem(i18n("All Desktops")); + +#ifdef KWIN_BUILD_ACTIVITIES + m_activities = new KActivities::Consumer(this); + connect(m_activities, &KActivities::Consumer::activitiesChanged, + this, [this] { updateActivitiesList(); }); + connect(m_activities, &KActivities::Consumer::serviceStatusChanged, + this, [this] { updateActivitiesList(); }); + updateActivitiesList(); +#endif + + KColorSchemeManager *schemes = new KColorSchemeManager(this); + decocolor->setModel(schemes->model()); + + // hide autogrouping as it's currently not supported + // BUG 370301 + line_11->hide(); + enable_autogroup->hide(); + autogroup->hide(); + rule_autogroup->hide(); + enable_autogroupid->hide(); + autogroupid->hide(); + rule_autogroupid->hide(); + enable_autogroupfg->hide(); + autogroupfg->hide(); + rule_autogroupfg->hide(); +} + +#undef SETUP + +#define UPDATE_ENABLE_SLOT(var) \ + void RulesWidget::updateEnable##var() \ + { \ + /* leave the label readable label_##var->setEnabled( enable_##var->isChecked() && rule_##var->currentIndex() != 0 );*/ \ + Ui::RulesWidgetBase::var->setEnabled( enable_##var->isChecked() && rule_##var->currentIndex() != 0 ); \ + } + +// geometry tab +UPDATE_ENABLE_SLOT(position) +UPDATE_ENABLE_SLOT(size) +UPDATE_ENABLE_SLOT(desktop) +UPDATE_ENABLE_SLOT(screen) +#ifdef KWIN_BUILD_ACTIVITIES +UPDATE_ENABLE_SLOT(activity) +#endif +UPDATE_ENABLE_SLOT(maximizehoriz) +UPDATE_ENABLE_SLOT(maximizevert) +UPDATE_ENABLE_SLOT(minimize) +UPDATE_ENABLE_SLOT(shade) +UPDATE_ENABLE_SLOT(fullscreen) +UPDATE_ENABLE_SLOT(placement) +// preferences tab +UPDATE_ENABLE_SLOT(above) +UPDATE_ENABLE_SLOT(below) +UPDATE_ENABLE_SLOT(noborder) +UPDATE_ENABLE_SLOT(decocolor) +UPDATE_ENABLE_SLOT(skiptaskbar) +UPDATE_ENABLE_SLOT(skippager) +UPDATE_ENABLE_SLOT(skipswitcher) +UPDATE_ENABLE_SLOT(acceptfocus) +UPDATE_ENABLE_SLOT(closeable) +UPDATE_ENABLE_SLOT(autogroup) +UPDATE_ENABLE_SLOT(autogroupfg) +UPDATE_ENABLE_SLOT(autogroupid) +UPDATE_ENABLE_SLOT(opacityactive) +UPDATE_ENABLE_SLOT(opacityinactive) +void RulesWidget::updateEnableshortcut() +{ + shortcut->setEnabled(enable_shortcut->isChecked() && rule_shortcut->currentIndex() != 0); + shortcut_edit->setEnabled(enable_shortcut->isChecked() && rule_shortcut->currentIndex() != 0); +} +// workarounds tab +UPDATE_ENABLE_SLOT(fsplevel) +UPDATE_ENABLE_SLOT(fpplevel) +UPDATE_ENABLE_SLOT(type) +UPDATE_ENABLE_SLOT(ignoregeometry) +UPDATE_ENABLE_SLOT(minsize) +UPDATE_ENABLE_SLOT(maxsize) +UPDATE_ENABLE_SLOT(strictgeometry) +UPDATE_ENABLE_SLOT(disableglobalshortcuts) +UPDATE_ENABLE_SLOT(blockcompositing) +UPDATE_ENABLE_SLOT(desktopfile) + +#undef UPDATE_ENABLE_SLOT + +static const int set_rule_to_combo[] = { + 0, // Unused + 0, // Don't Affect + 3, // Force + 1, // Apply + 2, // Remember + 4, // ApplyNow + 5 // ForceTemporarily +}; + +static const Rules::SetRule combo_to_set_rule[] = { + (Rules::SetRule)Rules::DontAffect, + (Rules::SetRule)Rules::Apply, + (Rules::SetRule)Rules::Remember, + (Rules::SetRule)Rules::Force, + (Rules::SetRule)Rules::ApplyNow, + (Rules::SetRule)Rules::ForceTemporarily +}; + +static const int force_rule_to_combo[] = { + 0, // Unused + 0, // Don't Affect + 1, // Force + 0, // Apply + 0, // Remember + 0, // ApplyNow + 2 // ForceTemporarily +}; + +static const Rules::ForceRule combo_to_force_rule[] = { + (Rules::ForceRule)Rules::DontAffect, + (Rules::ForceRule)Rules::Force, + (Rules::ForceRule)Rules::ForceTemporarily +}; + +static QString positionToStr(const QPoint& p) +{ + if (p == invalidPoint) + return QString(); + return QString::number(p.x()) + ',' + QString::number(p.y()); +} + +static QPoint strToPosition(const QString& str) +{ + // two numbers, with + or -, separated by any of , x X : + QRegExp reg("\\s*([+-]?[0-9]*)\\s*[,xX:]\\s*([+-]?[0-9]*)\\s*"); + if (!reg.exactMatch(str)) + return invalidPoint; + return QPoint(reg.cap(1).toInt(), reg.cap(2).toInt()); +} + +static QString sizeToStr(const QSize& s) +{ + if (!s.isValid()) + return QString(); + return QString::number(s.width()) + ',' + QString::number(s.height()); +} + +static QSize strToSize(const QString& str) +{ + // two numbers, with + or -, separated by any of , x X : + QRegExp reg("\\s*([+-]?[0-9]*)\\s*[,xX:]\\s*([+-]?[0-9]*)\\s*"); + if (!reg.exactMatch(str)) + return QSize(); + return QSize(reg.cap(1).toInt(), reg.cap(2).toInt()); +} + +int RulesWidget::desktopToCombo(int d) const +{ + if (d >= 1 && d < desktop->count()) + return d - 1; + return desktop->count() - 1; // on all desktops +} + +int RulesWidget::comboToDesktop(int val) const +{ + if (val == desktop->count() - 1) + return NET::OnAllDesktops; + return val + 1; +} + +#ifdef KWIN_BUILD_ACTIVITIES +int RulesWidget::activityToCombo(QString d) const +{ + // TODO: ivan - do a multiselection list + for (int i = 0; i < activity->count(); i++) { + if (activity->itemData(i).toString() == d) { + return i; + } + } + + return activity->count() - 1; // on all activities +} + +QString RulesWidget::comboToActivity(int val) const +{ + // TODO: ivan - do a multiselection list + if (val < 0 || val >= activity->count()) + return QString(); + + return activity->itemData(val).toString(); +} + +void RulesWidget::updateActivitiesList() +{ + activity->clear(); + + // cloned from kactivities/src/lib/core/consumer.cpp + #define NULL_UUID "00000000-0000-0000-0000-000000000000" + activity->addItem(i18n("All Activities"), QString::fromLatin1(NULL_UUID)); + #undef NULL_UUID + + if (m_activities->serviceStatus() == KActivities::Consumer::Running) { + foreach (const QString & activityId, m_activities->activities(KActivities::Info::Running)) { + const KActivities::Info info(activityId); + activity->addItem(info.name(), activityId); + } + } + + auto rules = this->rules(); + if (rules->activityrule == Rules::UnusedSetRule) { + enable_activity->setChecked(false); + Ui::RulesWidgetBase::activity->setCurrentIndex(0); + } else { + enable_activity->setChecked(true); + Ui::RulesWidgetBase::activity->setCurrentIndex(activityToCombo(m_selectedActivityId)); + } + updateEnableactivity(); +} +#endif + +static int placementToCombo(Placement::Policy placement) +{ + static const int conv[] = { + 1, // NoPlacement + 0, // Default + 0, // Unknown + 6, // Random + 2, // Smart + 4, // Cascade + 5, // Centered + 7, // ZeroCornered + 8, // UnderMouse + 9, // OnMainWindow + 3 // Maximizing + }; + return conv[ placement ]; +} + +static Placement::Policy comboToPlacement(int val) +{ + static const Placement::Policy conv[] = { + Placement::Default, + Placement::NoPlacement, + Placement::Smart, + Placement::Maximizing, + Placement::Cascade, + Placement::Centered, + Placement::Random, + Placement::ZeroCornered, + Placement::UnderMouse, + Placement::OnMainWindow + // no Placement::Unknown + }; + return conv[ val ]; +} + +static int typeToCombo(NET::WindowType type) +{ + if (type < NET::Normal || type > NET::Splash || + type == NET::Override) // The user must NOT set a window to be unmanaged. + // This case is not handled in KWin and will lead to segfaults. + // Even iff it was supported, it would mean to allow the user to shoot himself + // since an unmanaged window has to manage itself, what is probably not the case when the hint is not set. + // Rule opportunity might be a relict from the Motif Hint window times of KDE1 + return 0; // Normal + static const int conv[] = { + 0, // Normal + 7, // Desktop + 3, // Dock + 4, // Toolbar + 5, // Menu + 1, // Dialog + 8, // Override - ignored. + 9, // TopMenu + 2, // Utility + 6 // Splash + }; + return conv[ type ]; +} + +static NET::WindowType comboToType(int val) +{ + static const NET::WindowType conv[] = { + NET::Normal, + NET::Dialog, + NET::Utility, + NET::Dock, + NET::Toolbar, + NET::Menu, + NET::Splash, + NET::Desktop, + NET::TopMenu + }; + return conv[ val ]; +} + +#define GENERIC_RULE( var, func, Type, type, uimethod, uimethod0 ) \ + if ( rules->var##rule == Rules::Unused##Type##Rule ) \ + { \ + enable_##var->setChecked( false ); \ + rule_##var->setCurrentIndex( 0 ); \ + Ui::RulesWidgetBase::var->uimethod0; \ + updateEnable##var(); \ + } \ + else \ + { \ + enable_##var->setChecked( true ); \ + rule_##var->setCurrentIndex( type##_rule_to_combo[ rules->var##rule ] ); \ + Ui::RulesWidgetBase::var->uimethod( func( rules->var )); \ + updateEnable##var(); \ + } + +#define CHECKBOX_SET_RULE( var, func ) GENERIC_RULE( var, func, Set, set, setChecked, setChecked( false )) +#define LINEEDIT_SET_RULE( var, func ) GENERIC_RULE( var, func, Set, set, setText, setText( QString() )) +#define COMBOBOX_SET_RULE( var, func ) GENERIC_RULE( var, func, Set, set, setCurrentIndex, setCurrentIndex( 0 )) +#define SPINBOX_SET_RULE( var, func ) GENERIC_RULE( var, func, Set, set, setValue, setValue(0)) +#define CHECKBOX_FORCE_RULE( var, func ) GENERIC_RULE( var, func, Force, force, setChecked, setChecked( false )) +#define LINEEDIT_FORCE_RULE( var, func ) GENERIC_RULE( var, func, Force, force, setText, setText( QString() )) +#define COMBOBOX_FORCE_RULE( var, func ) GENERIC_RULE( var, func, Force, force, setCurrentIndex, setCurrentIndex( 0 )) +#define SPINBOX_FORCE_RULE( var, func ) GENERIC_RULE( var, func, Force, force, setValue, setValue(0)) + +void RulesWidget::setRules(Rules* rules) +{ + Rules tmp; + if (rules == nullptr) + rules = &tmp; // empty + description->setText(rules->description); + wmclass->setText(rules->wmclass); + whole_wmclass->setChecked(rules->wmclasscomplete); + wmclass_match->setCurrentIndex(rules->wmclassmatch); + wmclassMatchChanged(); + role->setText(rules->windowrole); + role_match->setCurrentIndex(rules->windowrolematch); + roleMatchChanged(); + types->item(0)->setSelected(rules->types & NET::NormalMask); + types->item(1)->setSelected(rules->types & NET::DialogMask); + types->item(2)->setSelected(rules->types & NET::UtilityMask); + types->item(3)->setSelected(rules->types & NET::DockMask); + types->item(4)->setSelected(rules->types & NET::ToolbarMask); + types->item(5)->setSelected(rules->types & NET::MenuMask); + types->item(6)->setSelected(rules->types & NET::SplashMask); + types->item(7)->setSelected(rules->types & NET::DesktopMask); + types->item(8)->setSelected(rules->types & NET::OverrideMask); + types->item(9)->setSelected(rules->types & NET::TopMenuMask); + title->setText(rules->title); + title_match->setCurrentIndex(rules->titlematch); + titleMatchChanged(); + machine->setText(rules->clientmachine); + machine_match->setCurrentIndex(rules->clientmachinematch); + machineMatchChanged(); + LINEEDIT_SET_RULE(position, positionToStr); + LINEEDIT_SET_RULE(size, sizeToStr); + COMBOBOX_SET_RULE(desktop, desktopToCombo); + SPINBOX_SET_RULE(screen, inc); +#ifdef KWIN_BUILD_ACTIVITIES + m_selectedActivityId = rules->activity; + COMBOBOX_SET_RULE(activity, activityToCombo); +#endif + CHECKBOX_SET_RULE(maximizehoriz,); + CHECKBOX_SET_RULE(maximizevert,); + CHECKBOX_SET_RULE(minimize,); + CHECKBOX_SET_RULE(shade,); + CHECKBOX_SET_RULE(fullscreen,); + COMBOBOX_FORCE_RULE(placement, placementToCombo); + CHECKBOX_SET_RULE(above,); + CHECKBOX_SET_RULE(below,); + CHECKBOX_SET_RULE(noborder,); + auto decoColorToCombo = [this](const QString &value) { + for (int i = 0; i < decocolor->count(); ++i) { + if (decocolor->itemData(i).toString() == value) { + return i; + } + } + // search for Breeze + for (int i = 0; i < decocolor->count(); ++i) { + if (QFileInfo(decocolor->itemData(i).toString()).baseName() == QStringLiteral("Breeze")) { + return i; + } + } + return 0; + }; + COMBOBOX_FORCE_RULE(decocolor, decoColorToCombo); + CHECKBOX_SET_RULE(skiptaskbar,); + CHECKBOX_SET_RULE(skippager,); + CHECKBOX_SET_RULE(skipswitcher,); + CHECKBOX_FORCE_RULE(acceptfocus,); + CHECKBOX_FORCE_RULE(closeable,); + CHECKBOX_FORCE_RULE(autogroup,); + CHECKBOX_FORCE_RULE(autogroupfg,); + LINEEDIT_FORCE_RULE(autogroupid,); + SPINBOX_FORCE_RULE(opacityactive,); + SPINBOX_FORCE_RULE(opacityinactive,); + LINEEDIT_SET_RULE(shortcut,); + COMBOBOX_FORCE_RULE(fsplevel,); + COMBOBOX_FORCE_RULE(fpplevel,); + COMBOBOX_FORCE_RULE(type, typeToCombo); + CHECKBOX_SET_RULE(ignoregeometry,); + LINEEDIT_FORCE_RULE(minsize, sizeToStr); + LINEEDIT_FORCE_RULE(maxsize, sizeToStr); + CHECKBOX_FORCE_RULE(strictgeometry,); + CHECKBOX_FORCE_RULE(disableglobalshortcuts,); + CHECKBOX_FORCE_RULE(blockcompositing,); + LINEEDIT_SET_RULE(desktopfile,) +} + +#undef GENERIC_RULE +#undef CHECKBOX_SET_RULE +#undef LINEEDIT_SET_RULE +#undef COMBOBOX_SET_RULE +#undef SPINBOX_SET_RULE +#undef CHECKBOX_FORCE_RULE +#undef LINEEDIT_FORCE_RULE +#undef COMBOBOX_FORCE_RULE +#undef SPINBOX_FORCE_RULE + +#define GENERIC_RULE( var, func, Type, type, uimethod ) \ + if ( enable_##var->isChecked() && rule_##var->currentIndex() >= 0) \ + { \ + rules->var##rule = combo_to_##type##_rule[ rule_##var->currentIndex() ]; \ + rules->var = func( Ui::RulesWidgetBase::var->uimethod()); \ + } \ + else \ + rules->var##rule = Rules::Unused##Type##Rule; + +#define CHECKBOX_SET_RULE( var, func ) GENERIC_RULE( var, func, Set, set, isChecked ) +#define LINEEDIT_SET_RULE( var, func ) GENERIC_RULE( var, func, Set, set, text ) +#define COMBOBOX_SET_RULE( var, func ) GENERIC_RULE( var, func, Set, set, currentIndex ) +#define SPINBOX_SET_RULE( var, func ) GENERIC_RULE( var, func, Set, set, value) +#define CHECKBOX_FORCE_RULE( var, func ) GENERIC_RULE( var, func, Force, force, isChecked ) +#define LINEEDIT_FORCE_RULE( var, func ) GENERIC_RULE( var, func, Force, force, text ) +#define COMBOBOX_FORCE_RULE( var, func ) GENERIC_RULE( var, func, Force, force, currentIndex ) +#define SPINBOX_FORCE_RULE( var, func ) GENERIC_RULE( var, func, Force, force, value) + +Rules* RulesWidget::rules() const +{ + Rules* rules = new Rules(); + rules->description = description->text(); + rules->wmclass = wmclass->text().toUtf8(); + rules->wmclasscomplete = whole_wmclass->isChecked(); + rules->wmclassmatch = static_cast< Rules::StringMatch >(wmclass_match->currentIndex()); + rules->windowrole = role->text().toUtf8(); + rules->windowrolematch = static_cast< Rules::StringMatch >(role_match->currentIndex()); + rules->types = 0; + bool all_types = true; + for (int i = 0; + i < types->count(); + ++i) + if (!types->item(i)->isSelected()) + all_types = false; + if (all_types) // if all types are selected, use AllTypesMask (for future expansion) + rules->types = NET::AllTypesMask; + else { + rules->types |= types->item(0)->isSelected() ? NET::NormalMask : NET::WindowTypeMask(0); + rules->types |= types->item(1)->isSelected() ? NET::DialogMask : NET::WindowTypeMask(0); + rules->types |= types->item(2)->isSelected() ? NET::UtilityMask : NET::WindowTypeMask(0); + rules->types |= types->item(3)->isSelected() ? NET::DockMask : NET::WindowTypeMask(0); + rules->types |= types->item(4)->isSelected() ? NET::ToolbarMask : NET::WindowTypeMask(0); + rules->types |= types->item(5)->isSelected() ? NET::MenuMask : NET::WindowTypeMask(0); + rules->types |= types->item(6)->isSelected() ? NET::SplashMask : NET::WindowTypeMask(0); + rules->types |= types->item(7)->isSelected() ? NET::DesktopMask : NET::WindowTypeMask(0); + rules->types |= types->item(8)->isSelected() ? NET::OverrideMask : NET::WindowTypeMask(0); + rules->types |= types->item(9)->isSelected() ? NET::TopMenuMask : NET::WindowTypeMask(0); + } + rules->title = title->text(); + rules->titlematch = static_cast< Rules::StringMatch >(title_match->currentIndex()); + rules->clientmachine = machine->text().toUtf8(); + rules->clientmachinematch = static_cast< Rules::StringMatch >(machine_match->currentIndex()); + LINEEDIT_SET_RULE(position, strToPosition); + LINEEDIT_SET_RULE(size, strToSize); + COMBOBOX_SET_RULE(desktop, comboToDesktop); + SPINBOX_SET_RULE(screen, dec); +#ifdef KWIN_BUILD_ACTIVITIES + COMBOBOX_SET_RULE(activity, comboToActivity); +#endif + CHECKBOX_SET_RULE(maximizehoriz,); + CHECKBOX_SET_RULE(maximizevert,); + CHECKBOX_SET_RULE(minimize,); + CHECKBOX_SET_RULE(shade,); + CHECKBOX_SET_RULE(fullscreen,); + COMBOBOX_FORCE_RULE(placement, comboToPlacement); + CHECKBOX_SET_RULE(above,); + CHECKBOX_SET_RULE(below,); + CHECKBOX_SET_RULE(noborder,); + auto comboToDecocolor = [this](int index) -> QString { + return QFileInfo(decocolor->itemData(index).toString()).baseName(); + }; + COMBOBOX_FORCE_RULE(decocolor, comboToDecocolor); + CHECKBOX_SET_RULE(skiptaskbar,); + CHECKBOX_SET_RULE(skippager,); + CHECKBOX_SET_RULE(skipswitcher,); + CHECKBOX_FORCE_RULE(acceptfocus,); + CHECKBOX_FORCE_RULE(closeable,); + CHECKBOX_FORCE_RULE(autogroup,); + CHECKBOX_FORCE_RULE(autogroupfg,); + LINEEDIT_FORCE_RULE(autogroupid,); + SPINBOX_FORCE_RULE(opacityactive,); + SPINBOX_FORCE_RULE(opacityinactive,); + LINEEDIT_SET_RULE(shortcut,); + COMBOBOX_FORCE_RULE(fsplevel,); + COMBOBOX_FORCE_RULE(fpplevel,); + COMBOBOX_FORCE_RULE(type, comboToType); + CHECKBOX_SET_RULE(ignoregeometry,); + LINEEDIT_FORCE_RULE(minsize, strToSize); + LINEEDIT_FORCE_RULE(maxsize, strToSize); + CHECKBOX_FORCE_RULE(strictgeometry,); + CHECKBOX_FORCE_RULE(disableglobalshortcuts,); + CHECKBOX_FORCE_RULE(blockcompositing,); + LINEEDIT_SET_RULE(desktopfile,); + return rules; +} + +#undef GENERIC_RULE +#undef CHECKBOX_SET_RULE +#undef LINEEDIT_SET_RULE +#undef COMBOBOX_SET_RULE +#undef SPINBOX_SET_RULE +#undef CHECKBOX_FORCE_RULE +#undef LINEEDIT_FORCE_RULE +#undef COMBOBOX_FORCE_RULE +#undef SPINBOX_FORCE_RULE + +#define STRING_MATCH_COMBO( type ) \ + void RulesWidget::type##MatchChanged() \ + { \ + edit_reg_##type->setEnabled( type##_match->currentIndex() == Rules::RegExpMatch ); \ + type->setEnabled( type##_match->currentIndex() != Rules::UnimportantMatch ); \ + } + +STRING_MATCH_COMBO(wmclass) +STRING_MATCH_COMBO(role) +STRING_MATCH_COMBO(title) +STRING_MATCH_COMBO(machine) + +#undef STRING_MATCH_COMBO + +void RulesWidget::detectClicked() +{ + assert(detect_dlg == nullptr); + detect_dlg = new DetectDialog; + connect(detect_dlg, SIGNAL(detectionDone(bool)), this, SLOT(detected(bool))); + detect_dlg->detect(Ui::RulesWidgetBase::detection_delay->value()); + Ui::RulesWidgetBase::detect->setEnabled(false); +} + +void RulesWidget::detected(bool ok) +{ + if (ok) { + wmclass->setText(detect_dlg->selectedClass()); + wmclass_match->setCurrentIndex(Rules::ExactMatch); + wmclassMatchChanged(); // grrr + whole_wmclass->setChecked(detect_dlg->selectedWholeClass()); + role->setText(detect_dlg->selectedRole()); + role_match->setCurrentIndex(detect_dlg->selectedRole().isEmpty() + ? Rules::UnimportantMatch : Rules::ExactMatch); + roleMatchChanged(); + if (detect_dlg->selectedWholeApp()) { + for (int i = 0; + i < types->count(); + ++i) + types->item(i)->setSelected(true); + } else { + NET::WindowType type = detect_dlg->selectedType(); + for (int i = 0; + i < types->count(); + ++i) + types->item(i)->setSelected(false); + types->item(typeToCombo(type))->setSelected(true); + } + title->setText(detect_dlg->selectedTitle()); + title_match->setCurrentIndex(detect_dlg->titleMatch()); + titleMatchChanged(); + machine->setText(detect_dlg->selectedMachine()); + machine_match->setCurrentIndex(Rules::UnimportantMatch); + machineMatchChanged(); + // prefill values from to window to settings which already set + prefillUnusedValues(detect_dlg->windowInfo()); + } + delete detect_dlg; + detect_dlg = nullptr; + detect_dlg_ok = ok; + Ui::RulesWidgetBase::detect->setEnabled(true); +} + +#define GENERIC_PREFILL( var, func, info, uimethod ) \ + if ( !enable_##var->isChecked()) \ + { \ + Ui::RulesWidgetBase::var->uimethod( func( info )); \ + } + +#define CHECKBOX_PREFILL( var, func, info ) GENERIC_PREFILL( var, func, info, setChecked ) +#define LINEEDIT_PREFILL( var, func, info ) GENERIC_PREFILL( var, func, info, setText ) +#define COMBOBOX_PREFILL( var, func, info ) GENERIC_PREFILL( var, func, info, setCurrentIndex ) +#define SPINBOX_PREFILL( var, func, info ) GENERIC_PREFILL( var, func, info, setValue ) + +void RulesWidget::prefillUnusedValues(const KWindowInfo& info) +{ + LINEEDIT_PREFILL(position, positionToStr, info.frameGeometry().topLeft()); + LINEEDIT_PREFILL(size, sizeToStr, info.frameGeometry().size()); + COMBOBOX_PREFILL(desktop, desktopToCombo, info.desktop()); + // COMBOBOX_PREFILL(activity, activityToCombo, info.activity()); // TODO: ivan + CHECKBOX_PREFILL(maximizehoriz, , info.state() & NET::MaxHoriz); + CHECKBOX_PREFILL(maximizevert, , info.state() & NET::MaxVert); + CHECKBOX_PREFILL(minimize, , info.isMinimized()); + CHECKBOX_PREFILL(shade, , info.state() & NET::Shaded); + CHECKBOX_PREFILL(fullscreen, , info.state() & NET::FullScreen); + //COMBOBOX_PREFILL( placement, placementToCombo ); + CHECKBOX_PREFILL(above, , info.state() & NET::KeepAbove); + CHECKBOX_PREFILL(below, , info.state() & NET::KeepBelow); + // noborder is only internal KWin information, so let's guess + CHECKBOX_PREFILL(noborder, , info.frameGeometry() == info.geometry()); + CHECKBOX_PREFILL(skiptaskbar, , info.state() & NET::SkipTaskbar); + CHECKBOX_PREFILL(skippager, , info.state() & NET::SkipPager); + CHECKBOX_PREFILL(skipswitcher, , info.state() & NET::SkipSwitcher); + //CHECKBOX_PREFILL( acceptfocus, ); + //CHECKBOX_PREFILL( closeable, ); + //CHECKBOX_PREFILL( autogroup, ); + //CHECKBOX_PREFILL( autogroupfg, ); + //LINEEDIT_PREFILL( autogroupid, ); + SPINBOX_PREFILL(opacityactive, , 100 /*get the actual opacity somehow*/); + SPINBOX_PREFILL(opacityinactive, , 100 /*get the actual opacity somehow*/); + //LINEEDIT_PREFILL( shortcut, ); + //COMBOBOX_PREFILL( fsplevel, ); + //COMBOBOX_PREFILL( fpplevel, ); + COMBOBOX_PREFILL(type, typeToCombo, info.windowType(SUPPORTED_MANAGED_WINDOW_TYPES_MASK)); + //CHECKBOX_PREFILL( ignoregeometry, ); + LINEEDIT_PREFILL(minsize, sizeToStr, info.frameGeometry().size()); + LINEEDIT_PREFILL(maxsize, sizeToStr, info.frameGeometry().size()); + //CHECKBOX_PREFILL( strictgeometry, ); + //CHECKBOX_PREFILL( disableglobalshortcuts, ); + //CHECKBOX_PREFILL( blockcompositing, ); + LINEEDIT_PREFILL(desktopfile, , info.desktopFileName()); +} + +void RulesWidget::prefillUnusedValues(const QVariantMap& info) +{ + const QSize windowSize{info.value("width").toInt(), info.value("height").toInt()}; + LINEEDIT_PREFILL(position, positionToStr, QPoint(info.value("x").toInt(), info.value("y").toInt())); + LINEEDIT_PREFILL(size, sizeToStr, windowSize); + COMBOBOX_PREFILL(desktop, desktopToCombo, info.value("x11DesktopNumber").toInt()); + // COMBOBOX_PREFILL(activity, activityToCombo, info.activity()); // TODO: ivan + CHECKBOX_PREFILL(maximizehoriz, , info.value("maximizeHorizontal").toBool()); + CHECKBOX_PREFILL(maximizevert, , info.value("maximizeVertical").toBool()); + CHECKBOX_PREFILL(minimize, , info.value("minimized").toBool()); + CHECKBOX_PREFILL(shade, , info.value("shaded").toBool()); + CHECKBOX_PREFILL(fullscreen, , info.value("fullscreen").toBool()); + //COMBOBOX_PREFILL( placement, placementToCombo ); + CHECKBOX_PREFILL(above, , info.value("keepAbove").toBool()); + CHECKBOX_PREFILL(below, , info.value("keepBelow").toBool()); + CHECKBOX_PREFILL(noborder, , info.value("noBorder").toBool()); + CHECKBOX_PREFILL(skiptaskbar, , info.value("skipTaskbar").toBool()); + CHECKBOX_PREFILL(skippager, , info.value("skipPager").toBool()); + CHECKBOX_PREFILL(skipswitcher, , info.value("skipSwitcher").toBool()); + //CHECKBOX_PREFILL( acceptfocus, ); + //CHECKBOX_PREFILL( closeable, ); + //CHECKBOX_PREFILL( autogroup, ); + //CHECKBOX_PREFILL( autogroupfg, ); + //LINEEDIT_PREFILL( autogroupid, ); + SPINBOX_PREFILL(opacityactive, , 100 /*get the actual opacity somehow*/); + SPINBOX_PREFILL(opacityinactive, , 100 /*get the actual opacity somehow*/); + //LINEEDIT_PREFILL( shortcut, ); + //COMBOBOX_PREFILL( fsplevel, ); + //COMBOBOX_PREFILL( fpplevel, ); + COMBOBOX_PREFILL(type, typeToCombo, info.value("type").value()); + //CHECKBOX_PREFILL( ignoregeometry, ); + LINEEDIT_PREFILL(minsize, sizeToStr, windowSize); + LINEEDIT_PREFILL(maxsize, sizeToStr, windowSize); + //CHECKBOX_PREFILL( strictgeometry, ); + //CHECKBOX_PREFILL( disableglobalshortcuts, ); + //CHECKBOX_PREFILL( blockcompositing, ); + LINEEDIT_PREFILL(desktopfile, , info.value("desktopFile").toString()); +} + +#undef GENERIC_PREFILL +#undef CHECKBOX_PREFILL +#undef LINEEDIT_PREFILL +#undef COMBOBOX_PREFILL +#undef SPINBOX_PREFILL + +bool RulesWidget::finalCheck() +{ + if (description->text().isEmpty()) { + if (!wmclass->text().isEmpty()) + description->setText(i18n("Settings for %1", wmclass->text())); + else + description->setText(i18n("Unnamed entry")); + } + bool all_types = true; + for (int i = 0; + i < types->count(); + ++i) + if (!types->item(i)->isSelected()) + all_types = false; + if (wmclass_match->currentIndex() == Rules::UnimportantMatch && all_types) { + if (KMessageBox::warningContinueCancel(window(), + 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.")) != KMessageBox::Continue) + return false; + } + return true; +} + +void RulesWidget::prepareWindowSpecific(WId window) +{ + // TODO: adjust for Wayland + tabs->setCurrentIndex(1); // geometry tab, skip tab for window identification + KWindowInfo info(window, NET::WMAllProperties, NET::WM2AllProperties); // read everything + prefillUnusedValues(info); +} + +void RulesWidget::shortcutEditClicked() +{ + QPointer dlg = new EditShortcutDialog(window()); + dlg->setShortcut(shortcut->text()); + if (dlg->exec() == QDialog::Accepted) + shortcut->setText(dlg->shortcut()); + delete dlg; +} + +RulesDialog::RulesDialog(QWidget* parent, const char* name) + : QDialog(parent) +{ + setObjectName(name); + setModal(true); + setWindowTitle(i18n("Edit Window-Specific Settings")); + setWindowIcon(QIcon::fromTheme("preferences-system-windows-actions")); + + setLayout(new QVBoxLayout); + widget = new RulesWidget(this); + layout()->addWidget(widget); + + 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, WId window, bool show_hints) +{ + rules = r; + widget->setRules(rules); + if (window != 0) + widget->prepareWindowSpecific(window); + if (show_hints) + QTimer::singleShot(0, this, SLOT(displayHints())); + exec(); + return rules; +} + +void RulesDialog::displayHints() +{ + QString str = "

"; + str += i18n("This configuration dialog allows altering settings only for the selected window" + " or application. Find the setting you want to affect, enable the setting using the checkbox," + " select in what way the setting should be affected and to which value."); +#if 0 // maybe later + str += "

" + i18n("Consult the documentation for more details."); +#endif + str += "

"; + KMessageBox::information(this, str, QString(), "displayhints"); +} + +void RulesDialog::accept() +{ + if (!widget->finalCheck()) + return; + rules = widget->rules(); + QDialog::accept(); +} + +EditShortcut::EditShortcut(QWidget* parent) + : QWidget(parent) +{ + setupUi(this); +} + +void EditShortcut::editShortcut() +{ + QPointer< ShortcutDialog > dlg = new ShortcutDialog(QKeySequence(shortcut->text()), window()); + if (dlg->exec() == QDialog::Accepted) + shortcut->setText(dlg->shortcut().toString()); + delete dlg; +} + +void EditShortcut::clearShortcut() +{ + shortcut->clear(); +} + +EditShortcutDialog::EditShortcutDialog(QWidget* parent, const char* name) + : QDialog(parent) + , widget(new EditShortcut(this)) +{ + setObjectName(name); + setModal(true); + setWindowTitle(i18n("Edit Shortcut")); + + setLayout(new QVBoxLayout); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttons, SIGNAL(accepted()), SLOT(accept())); + connect(buttons, SIGNAL(rejected()), SLOT(reject())); + + layout()->addWidget(widget); + layout()->addWidget(buttons); +} + +void EditShortcutDialog::setShortcut(const QString& cut) +{ + widget->shortcut->setText(cut); +} + +QString EditShortcutDialog::shortcut() const +{ + return widget->shortcut->text(); +} + +ShortcutDialog::ShortcutDialog(const QKeySequence& cut, QWidget* parent) + : QDialog(parent) + , widget(new KKeySequenceWidget(this)) +{ + widget->setKeySequence(cut); + // It's a global shortcut so don't allow multikey shortcuts + widget->setMultiKeyShortcutsAllowed(false); + + QDialogButtonBox* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this); + connect(buttons, SIGNAL(accepted()), SLOT(accept())); + connect(buttons, SIGNAL(rejected()), SLOT(reject())); + + setLayout(new QVBoxLayout); + layout()->addWidget(widget); + layout()->addWidget(buttons); +} + +void ShortcutDialog::accept() +{ + QKeySequence seq = shortcut(); + if (!seq.isEmpty()) { + if (seq[0] == Qt::Key_Escape) { + reject(); + return; + } + if (seq[0] == Qt::Key_Space + || (seq[0] & Qt::KeyboardModifierMask) == 0) { + // clear + widget->clearKeySequence(); + QDialog::accept(); + return; + } + } + QDialog::accept(); +} + +QKeySequence ShortcutDialog::shortcut() const +{ + return widget->keySequence(); +} + +} // namespace + diff --git a/kcmkwin/kwinrules/ruleswidget.h b/kcmkwin/kwinrules/ruleswidget.h new file mode 100644 index 0000000..3f48c64 --- /dev/null +++ b/kcmkwin/kwinrules/ruleswidget.h @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2004 Lubos Lunak + * + * 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. + */ + + +#ifndef __RULESWIDGET_H__ +#define __RULESWIDGET_H__ + +#include + +#include +#include +#include + +#include "ui_ruleswidgetbase.h" +#include "ui_editshortcut.h" + +#ifdef KWIN_BUILD_ACTIVITIES +namespace KActivities { + class Consumer; +} // namespace KActivities +#endif + +namespace KWin +{ + +class Rules; +class DetectDialog; + +class RulesWidget + : public QWidget, public Ui::RulesWidgetBase +{ + Q_OBJECT +public: + explicit RulesWidget(QWidget* parent = nullptr); + void setRules(Rules* r); + Rules* rules() const; + bool finalCheck(); + void prepareWindowSpecific(WId window); +Q_SIGNALS: + void changed(bool state); +protected Q_SLOTS: + void detectClicked(); + void wmclassMatchChanged(); + void roleMatchChanged(); + void titleMatchChanged(); + void machineMatchChanged(); + void shortcutEditClicked(); +private Q_SLOTS: + // geometry tab + void updateEnableposition(); + void updateEnablesize(); + void updateEnabledesktop(); + void updateEnablescreen(); +#ifdef KWIN_BUILD_ACTIVITIES + void updateEnableactivity(); +#endif + void updateEnablemaximizehoriz(); + void updateEnablemaximizevert(); + void updateEnableminimize(); + void updateEnableshade(); + void updateEnablefullscreen(); + void updateEnableplacement(); + // preferences tab + void updateEnableabove(); + void updateEnablebelow(); + void updateEnablenoborder(); + void updateEnabledecocolor(); + void updateEnableskiptaskbar(); + void updateEnableskippager(); + void updateEnableskipswitcher(); + void updateEnableacceptfocus(); + void updateEnablecloseable(); + void updateEnableautogroup(); + void updateEnableautogroupfg(); + void updateEnableautogroupid(); + void updateEnableopacityactive(); + void updateEnableopacityinactive(); + // workarounds tab + void updateEnablefsplevel(); + void updateEnablefpplevel(); + void updateEnabletype(); + void updateEnableignoregeometry(); + void updateEnableminsize(); + void updateEnablemaxsize(); + void updateEnablestrictgeometry(); + void updateEnableshortcut(); + void updateEnabledisableglobalshortcuts(); + void updateEnableblockcompositing(); + void updateEnabledesktopfile(); + // internal + void detected(bool); +private: + int desktopToCombo(int d) const; + int comboToDesktop(int val) const; +#ifdef KWIN_BUILD_ACTIVITIES + int activityToCombo(QString d) const; + QString comboToActivity(int val) const; + void updateActivitiesList(); + KActivities::Consumer *m_activities; + QString m_selectedActivityId; // we need this for async activity loading +#endif + int comboToTiling(int val) const; + int inc(int i) const { return i+1; } + int dec(int i) const { return i-1; } + void prefillUnusedValues(const KWindowInfo& info); + void prefillUnusedValues(const QVariantMap& info); + DetectDialog* detect_dlg; + bool detect_dlg_ok; +}; + +class RulesDialog + : public QDialog +{ + Q_OBJECT +public: + explicit RulesDialog(QWidget* parent = nullptr, const char* name = nullptr); + Rules* edit(Rules* r, WId window, bool show_hints); +protected: + virtual void accept(); +private Q_SLOTS: + void displayHints(); +private: + RulesWidget* widget; + Rules* rules; +}; + +class EditShortcut + : public QWidget, public Ui_EditShortcut +{ + Q_OBJECT +public: + explicit EditShortcut(QWidget* parent = nullptr); +protected Q_SLOTS: + void editShortcut(); + void clearShortcut(); +}; + +class EditShortcutDialog + : public QDialog +{ + Q_OBJECT +public: + explicit EditShortcutDialog(QWidget* parent = nullptr, const char* name = nullptr); + void setShortcut(const QString& cut); + QString shortcut() const; +private: + EditShortcut* widget; +}; + +// slightly duped from utils.cpp +class ShortcutDialog + : public QDialog +{ + Q_OBJECT +public: + explicit ShortcutDialog(const QKeySequence& cut, QWidget* parent = nullptr); + virtual void accept(); + QKeySequence shortcut() const; +private: + KKeySequenceWidget* widget; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwinrules/ruleswidgetbase.ui b/kcmkwin/kwinrules/ruleswidgetbase.ui new file mode 100644 index 0000000..6ca054f --- /dev/null +++ b/kcmkwin/kwinrules/ruleswidgetbase.ui @@ -0,0 +1,2834 @@ + + + KWin::RulesWidgetBase + + + + 0 + 0 + 592 + 588 + + + + + + + 0 + + + + &Window matching + + + + + + &Description: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + description + + + + + + + + + + Window &class (application): + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + wmclass + + + + + + + + Unimportant + + + + + Exact Match + + + + + Substring Match + + + + + Regular Expression + + + + + + + + + + + false + + + Edit + + + + + + + + + + Match w&hole window class + + + + + + + Window ro&le: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + role + + + + + + + + Unimportant + + + + + Exact Match + + + + + Substring Match + + + + + Regular Expression + + + + + + + + + + + false + + + Edit + + + + + + + Window &types: + + + Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + + + false + + + types + + + + + + + Window t&itle: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + title + + + + + + + + Unimportant + + + + + Exact Match + + + + + Substring Match + + + + + Regular Expression + + + + + + + + false + + + Edit + + + + + + + + + + &Machine (hostname): + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + false + + + machine + + + + + + + + Unimportant + + + + + Exact Match + + + + + Substring Match + + + + + Regular Expression + + + + + + + + false + + + Edit + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Detect Window Properties + + + + + + + s delay + + + 30 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 0 + 0 + + + + false + + + Qt::ScrollBarAsNeeded + + + Qt::ScrollBarAsNeeded + + + true + + + false + + + QAbstractItemView::NoDragDrop + + + false + + + QAbstractItemView::ExtendedSelection + + + QListView::Static + + + QListView::TopToBottom + + + true + + + QListView::Adjust + + + 0 + + + QListView::ListMode + + + true + + + + Normal Window + + + + + Dialog Window + + + + + Utility Window + + + + + Dock (panel) + + + + + Toolbar + + + + + Torn-Off Menu + + + + + Splash Screen + + + + + Desktop + + + + + Unmanaged Window + + + + + Standalone Menubar + + + + + + + + Qt::Horizontal + + + + + + + + &Size && Position + + + + + + &Position + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + x,y + + + 0123456789-+,xX: + + + + + + + &Size + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + width,height + + + 0123456789-+,xX: + + + + + + + Qt::Horizontal + + + + + + + Maximized &horizontally + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + + + + Maximized &vertically + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + + + + Qt::Horizontal + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + &Desktop + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Activit&y + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + + + + false + + + 1 + + + + + + + Qt::Horizontal + + + + + + + &Fullscreen + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + + + + M&inimized + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + + + + Sh&aded + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + + + + Qt::Horizontal + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + + Default + + + + + No Placement + + + + + Smart + + + + + Maximizing + + + + + Cascade + + + + + Centered + + + + + Random + + + + + Top-Left Corner + + + + + Under Mouse + + + + + On Main Window + + + + + + + + + + + Initial p&lacement + + + + + + + Windows can ask to appear in a certain position. +By default this overrides the placement strategy +what might be nasty if the client abuses the feature +to unconditionally popup in the middle of your screen. + + + Ignore requested &geometry + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + + + + Qt::Horizontal + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + M&inimum size + + + + + + + false + + + width,height + + + 0123456789-+,xX: + + + + + + + M&aximum size + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + width,height + + + 0123456789-+,xX: + + + + + + + Eg. terminals or video players can ask to keep a certain aspect ratio +or only grow by values larger than one +(eg. by the dimensions of one character). +This may be pointless and the restriction prevents arbitrary dimensions +like your complete screen area. + + + Qt::LeftToRight + + + Obey geometry restrictions + + + + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 16 + + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + Screen + + + + + + + + &Arrangement && Access + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + Window shall (not) appear in the manager for virtual desktops + + + Skip pa&ger + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + + + + false + + + + + + + Window shall (not) appear in the taskbar. + + + Skip &taskbar + + + + + + + false + + + + + + + Window shall (not) appear in the Alt+Tab list + + + Skip &switcher + + + + + + + false + + + Edit... + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + Qt::Horizontal + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 8 + + + + + + + + false + + + + + + + false + + + + + + + Shortcut + + + + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + + + + false + + + + + + + Keep &above + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Autog&roup in foreground + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + + + + + Qt::Horizontal + + + + + + + Keep &below + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + + 0 + 0 + + + + Qt::Horizontal + + + + + + + Autogroup by I&D + + + + + + + Autogroup with &identical + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + + + + + Appearance && &Fixes + + + + + + &No titlebar and frame + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + + + + + Titlebar color &scheme + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + + + + + Qt::Horizontal + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + A&ctive opacity + + + + + + + false + + + % + + + 100 + + + 100 + + + + + + + I&nactive opacity + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + % + + + 100 + + + 100 + + + + + + + Qt::Horizontal + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 8 + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 8 + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + KWin tries to prevent windows from taking the focus +("activate") while you're working in another window, +but this may sometimes fail or superact. +"None" will unconditionally allow this window to get the focus while +"Extreme" will completely prevent it from taking the focus. + + + &Focus stealing prevention + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + + None + + + + + Low + + + + + Normal + + + + + High + + + + + Extreme + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + This controls the focus protection of the currently active window. +None will always give the focus away, +Extreme will keep it. +Otherwise it's interleaved with the stealing prevention +assigned to the window that wants the focus. + + + Focus protection + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + + None + + + + + Low + + + + + Normal + + + + + High + + + + + Extreme + + + + + + + + Windows may prevent to get the focus (activate) when being clicked. +On the other hand you might wish to prevent a window +from getting focused on a mouse click. + + + Accept &focus + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + + + + + When used, a window will receive +all keyboard inputs while it is active, including Alt+Tab etc. +This is especially interesting for emulators or virtual machines. + +Be warned: +you won't be able to Alt+Tab out of the window +nor use any other global shortcut (such as Alt+F2 to show KRunner) +while it's active! + + + Ignore global shortcuts + + + + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + + + + + Qt::Horizontal + + + + + + + &Closeable + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + + + + + Window &type + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + + Normal Window + + + + + Dialog Window + + + + + Utility Window + + + + + Dock (panel) + + + + + Toolbar + + + + + Torn-Off Menu + + + + + Splash Screen + + + + + Desktop + + + + + Standalone Menubar + + + + + + + + Desktop file name + + + + + + + + + + false + + + + Do Not Affect + + + + + Apply Initially + + + + + Remember + + + + + Force + + + + + Apply Now + + + + + Force Temporarily + + + + + + + + false + + + org.kde.kwin + + + + + + + Qt::Horizontal + + + + + + + Block compositing + + + + + + + false + + + + Do Not Affect + + + + + Force + + + + + Force Temporarily + + + + + + + + false + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + KLineEdit + QLineEdit +
klineedit.h
+
+ + KComboBox + QComboBox +
kcombobox.h
+
+ + YesNoBox + QWidget +
yesnobox.h
+ 1 +
+
+ + description + detect + detection_delay + wmclass_match + wmclass + edit_reg_wmclass + whole_wmclass + role_match + role + edit_reg_role + types + title_match + title + edit_reg_title + machine_match + machine + edit_reg_machine + enable_position + rule_position + position + enable_size + rule_size + size + enable_maximizehoriz + rule_maximizehoriz + enable_maximizevert + rule_maximizevert + enable_desktop + rule_desktop + desktop + enable_activity + rule_activity + activity + enable_screen + rule_screen + screen + enable_fullscreen + rule_fullscreen + enable_minimize + rule_minimize + enable_shade + rule_shade + enable_placement + rule_placement + placement + enable_ignoregeometry + rule_ignoregeometry + enable_minsize + rule_minsize + minsize + enable_maxsize + rule_maxsize + maxsize + enable_strictgeometry + rule_strictgeometry + enable_above + rule_above + enable_below + rule_below + enable_autogroup + rule_autogroup + enable_autogroupfg + rule_autogroupfg + enable_autogroupid + rule_autogroupid + autogroupid + enable_skiptaskbar + rule_skiptaskbar + enable_skippager + rule_skippager + enable_skipswitcher + rule_skipswitcher + enable_shortcut + rule_shortcut + shortcut + shortcut_edit + enable_noborder + rule_noborder + enable_decocolor + rule_decocolor + decocolor + enable_opacityactive + rule_opacityactive + opacityactive + enable_opacityinactive + rule_opacityinactive + opacityinactive + enable_fsplevel + rule_fsplevel + fsplevel + enable_fpplevel + rule_fpplevel + fpplevel + enable_acceptfocus + rule_acceptfocus + enable_disableglobalshortcuts + rule_disableglobalshortcuts + enable_closeable + rule_closeable + enable_type + rule_type + type + enable_desktopfile + rule_desktopfile + desktopfile + enable_blockcompositing + rule_blockcompositing + tabs + + + + + detect + clicked() + KWin::RulesWidgetBase + detectClicked() + + + 285 + 124 + + + 20 + 20 + + + + + wmclass_match + activated(int) + KWin::RulesWidgetBase + wmclassMatchChanged() + + + 297 + 196 + + + 20 + 20 + + + + + role_match + activated(int) + KWin::RulesWidgetBase + roleMatchChanged() + + + 297 + 254 + + + 20 + 20 + + + + + title_match + activated(int) + KWin::RulesWidgetBase + titleMatchChanged() + + + 231 + 482 + + + 242 + 293 + + + + + machine_match + activated(int) + KWin::RulesWidgetBase + machineMatchChanged() + + + 194 + 509 + + + 242 + 293 + + + + +
diff --git a/kcmkwin/kwinrules/yesnobox.h b/kcmkwin/kwinrules/yesnobox.h new file mode 100644 index 0000000..222ff6e --- /dev/null +++ b/kcmkwin/kwinrules/yesnobox.h @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2011 Thomas Lübking + * + * 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. + */ + + +#ifndef YESNOBOX_H +#define YESNOBOX_H + +#include +#include + +#include + +class YesNoBox : public QWidget { + Q_OBJECT +public: + explicit YesNoBox( QWidget *parent ) : QWidget(parent) + { + QHBoxLayout *l = new QHBoxLayout(this); + l->setContentsMargins(0, 0, 0, 0); + l->addWidget(yes = new QRadioButton(i18n("Yes"), this)); + l->addWidget(no = new QRadioButton(i18n("No"), this)); + l->addStretch(100); + no->setChecked(true); + connect(yes, SIGNAL(clicked(bool)), this, SIGNAL(clicked(bool))); + connect(yes, SIGNAL(toggled(bool)), this, SIGNAL(toggled(bool))); + connect(no, SIGNAL(clicked(bool)), this, SLOT(noClicked(bool))); + } + bool isChecked() { return yes->isChecked(); } +public Q_SLOTS: + void setChecked(bool b) { yes->setChecked(b); } + void toggle() { yes->toggle(); } + +Q_SIGNALS: + void clicked(bool checked = false); + void toggled(bool checked); +private Q_SLOTS: + void noClicked(bool checked) { emit clicked(!checked); } +private: + QRadioButton *yes, *no; +}; + +#endif // YESNOBOX_H diff --git a/kcmkwin/kwinscreenedges/CMakeLists.txt b/kcmkwin/kwinscreenedges/CMakeLists.txt new file mode 100644 index 0000000..42506f6 --- /dev/null +++ b/kcmkwin/kwinscreenedges/CMakeLists.txt @@ -0,0 +1,36 @@ +# 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 + ) +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 ) +add_library( kcm_kwinscreenedges MODULE ${kcm_kwinscreenedges_PART_SRCS} ) +set(kcm_screenedges_LIBS + Qt5::DBus + KF5::Completion + KF5::ConfigCore + KF5::ConfigWidgets + KF5::I18n + KF5::Service + KF5::Package + KF5::Plasma + kwin4_effect_builtins +) +target_link_libraries( kcm_kwinscreenedges ${X11_LIBRARIES} ${kcm_screenedges_LIBS}) + +set(kcm_kwintouchscreenedges_PART_SRCS touch.cpp ${kcm_screenedges_SRCS}) +ki18n_wrap_ui( kcm_kwintouchscreenedges_PART_SRCS touch.ui ) +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/kwinscreenedges.desktop b/kcmkwin/kwinscreenedges/kwinscreenedges.desktop new file mode 100644 index 0000000..11b5f4b --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwinscreenedges.desktop @@ -0,0 +1,165 @@ +[Desktop Entry] +Exec=kcmshell5 kwinscreenedges +Icon=preferences-desktop +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[ast]=Berbesos de pantalla +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]=Screen Edges +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]=Muchiile 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=Active Screen Corners and Edges +Comment[bs]=Ivice i uglovi aktivnog ekrana +Comment[ca]=Cantonades i vores actives de la pantalla +Comment[ca@valencia]=Cantonades i vores actives de la pantalla +Comment[cs]=Aktivní rohy a hrany obrazovky +Comment[da]=Aktive skærmhjørner og -kanter +Comment[de]=Aktive Bildschirmränder +Comment[el]=Ενεργές γωνίες και άκρα οθόνης +Comment[en_GB]=Active Screen Corners and Edges +Comment[es]=Esquinas y bordes de la pantalla activa +Comment[et]=Aktiivsed ekraani nurgad ja servad +Comment[eu]=Pantaila aktiboaren izkinak eta ertzak +Comment[fi]=Näytön kulmien ja reunojen toiminnot +Comment[fr]=Bords et coins actifs de l'écran +Comment[gl]=Bordos e esquinas activos da pantalla +Comment[he]=פינות וקצוות של המסך +Comment[hu]=Aktív képernyősarkok és szélek +Comment[id]=Pojok dan Tepi Layar Aktif +Comment[it]=Angoli e bordi attivi dello schermo +Comment[ko]=활성 화면 경계와 꼭지점 +Comment[lt]=Aktyvaus ekrano kampai ir kraštinės +Comment[nb]=Aktive skjemkanter og hjørner +Comment[nds]=Aktive Schirmkanten un -hörns +Comment[nl]=Hoeken en randen van het actieve scherm +Comment[nn]=Aktive skjermhjørne og skjermkantar +Comment[pa]=ਸਰਗਰਮ ਸਕਰੀਨ ਕੋਨੇ ਅਤੇ ਬਾਹੀਆਂ +Comment[pl]=Narożniki i krawędzie aktywnego ekranu +Comment[pt]=Cantos e Extremos do Ecrã Activo +Comment[pt_BR]=Cantos e bordas da tela ativa +Comment[ru]=Настройка действий для краёв экрана +Comment[sk]=Rohy a okraje aktívneho okna +Comment[sl]=Dejavni robovi in koti zaslona +Comment[sr]=Активни углови и ивице екрана +Comment[sr@ijekavian]=Активни углови и ивице екрана +Comment[sr@ijekavianlatin]=Aktivni uglovi i ivice ekrana +Comment[sr@latin]=Aktivni uglovi i ivice ekrana +Comment[sv]=Aktiva skärmhörn och kanter +Comment[tr]=Etkin Ekran Kenar ve Köşeleri +Comment[uk]=Активні кути та краї екрана +Comment[x-test]=xxActive 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[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 pantalla del kwin,vores d'escriptori,vores de pantalla,maximitza finestres,mosaic de les finestres,costat de pantalla,comportament de pantalla,canvi d'escriptori,escriptori virtual,cantonades de la pantalla +X-KDE-Keywords[ca@valencia]=kwin,finestra,gestor,efecte,cantonada,vora,acció,canvi,escriptori,vores de pantalla del kwin,vores d'escriptori,vores de pantalla,maximitza finestres,mosaic de les finestres,costat de pantalla,comportament de pantalla,canvi d'escriptori,escriptori virtual,cantonades 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,jendela,pengelola,efek,sudut,tepi,batas,aksi,ganti,desktop,tepi layar kwin,tepi desktop,tepi layar,maksimalkan jendela,jendela ubin,tepi layar,perilaku layar,ganti desktop,desktop virtual,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[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/kwintouchscreen.desktop b/kcmkwin/kwinscreenedges/kwintouchscreen.desktop new file mode 100644 index 0000000..98b17d9 --- /dev/null +++ b/kcmkwin/kwinscreenedges/kwintouchscreen.desktop @@ -0,0 +1,120 @@ +[Desktop Entry] +Exec=kcmshell5 kwintouchscreen +Icon=preferences-desktop +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[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[eu]=Ukimen-pantaila +Name[fi]=Kosketusnäyttö +Name[fr]=Écran tactile +Name[gl]=Pantalla táctil +Name[he]=מסך מגע +Name[hu]=Érintőképernyő +Name[id]=Touch Screen +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[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=Touch screen swipe gestures +Comment[ca]=Gestos de lliscament en la pantalla tàctil +Comment[ca@valencia]=Gestos de lliscament en la pantalla tàctil +Comment[da]=Strygegestusser til touchskærm +Comment[de]=Wischgesten für Touchscreens +Comment[en_GB]=Touch screen swipe gestures +Comment[es]=Gestos de deslizamiento en pantalla táctil +Comment[eu]=Ukipen-pantailan irrist-keinuak +Comment[fi]=Kosketusnäytön pyyhkäisyeleet +Comment[fr]=Mouvements sur l'écran tactile +Comment[gl]=Xestos de pantalla táctil +Comment[he]=מחוות החלקה של מסכי מגע +Comment[hu]=Érintőképernyő-gesztusok +Comment[id]=Isyarat usapan layar sentuh +Comment[it]=Gesti dello schermo a sfioramento +Comment[ko]=터치 스크린 밀기 제스처 +Comment[nl]=Veeggebaren voor aanraakscherm +Comment[nn]=Fingerrørsler på trykkskjerm +Comment[pa]=ਟੱਚ ਸਕਰੀਨ ਸਕਰਾਉਣ ਜੈਸਚਰ +Comment[pl]=Gesty na ekranie dotykowym +Comment[pt]=Gestos para deslizar o ecrã táctil +Comment[pt_BR]=Gestos no touch screen +Comment[ru]=Действия при проведении по сенсорному экрану +Comment[sk]=Ťahacie gestá dotykovej obrazovky +Comment[sl]=Kretnje vlečenja za zaslon na dotik +Comment[sr]=Гестови замаха на додирнику +Comment[sr@ijekavian]=Гестови замаха на додирнику +Comment[sr@ijekavianlatin]=Gestovi zamaha na dodirniku +Comment[sr@latin]=Gestovi zamaha na dodirniku +Comment[sv]=Draggester för pekskärm +Comment[tr]=Dokunmatik ekran kaydırma hareketleri +Comment[uk]=Жести на сенсорній панелі +Comment[x-test]=xxTouch 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[ca]=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[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[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[id]=kwin,jendela,pengelola,efek,tepi,batas,aksi,alih,desktop,tepi desktop,tepi 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[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[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/main.cpp b/kcmkwin/kwinscreenedges/main.cpp new file mode 100644 index 0000000..6cc3016 --- /dev/null +++ b/kcmkwin/kwinscreenedges/main.cpp @@ -0,0 +1,535 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2008 Martin Gräßlin +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +#include "main.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +K_PLUGIN_FACTORY(KWinScreenEdgesConfigFactory, registerPlugin();) + +namespace KWin +{ + +KWinScreenEdgesConfigForm::KWinScreenEdgesConfigForm(QWidget* parent) + : QWidget(parent) +{ + setupUi(this); +} + +KWinScreenEdgesConfig::KWinScreenEdgesConfig(QWidget* parent, const QVariantList& args) + : KCModule(parent, args) + , m_config(KSharedConfig::openConfig("kwinrc")) +{ + m_ui = new KWinScreenEdgesConfigForm(this); + QVBoxLayout* layout = new QVBoxLayout(this); + layout->addWidget(m_ui); + + monitorInit(); + + connect(m_ui->monitor, SIGNAL(changed()), this, SLOT(changed())); + + connect(m_ui->desktopSwitchCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(changed())); + connect(m_ui->activationDelaySpin, SIGNAL(valueChanged(int)), this, SLOT(sanitizeCooldown())); + connect(m_ui->activationDelaySpin, SIGNAL(valueChanged(int)), this, SLOT(changed())); + connect(m_ui->triggerCooldownSpin, SIGNAL(valueChanged(int)), this, SLOT(changed())); + connect(m_ui->quickMaximizeBox, SIGNAL(stateChanged(int)), this, SLOT(changed())); + connect(m_ui->quickTileBox, SIGNAL(stateChanged(int)), this, SLOT(changed())); + connect(m_ui->electricBorderCornerRatio, SIGNAL(valueChanged(int)), this, SLOT(changed())); + + // Visual feedback of action group conflicts + connect(m_ui->desktopSwitchCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(groupChanged())); + connect(m_ui->quickMaximizeBox, SIGNAL(stateChanged(int)), this, SLOT(groupChanged())); + connect(m_ui->quickTileBox, SIGNAL(stateChanged(int)), this, SLOT(groupChanged())); + + load(); + + sanitizeCooldown(); +} + +KWinScreenEdgesConfig::~KWinScreenEdgesConfig() +{ +} + +void KWinScreenEdgesConfig::groupChanged() +{ + // Monitor conflicts + bool hide = false; + if (m_ui->desktopSwitchCombo->currentIndex() == 2) + hide = true; + monitorHideEdge(ElectricTop, hide); + monitorHideEdge(ElectricRight, hide); + monitorHideEdge(ElectricBottom, hide); + monitorHideEdge(ElectricLeft, hide); +} + +void KWinScreenEdgesConfig::load() +{ + KCModule::load(); + + monitorLoad(); + + KConfigGroup config(m_config, "Windows"); + + m_ui->desktopSwitchCombo->setCurrentIndex(config.readEntry("ElectricBorders", 0)); + m_ui->activationDelaySpin->setValue(config.readEntry("ElectricBorderDelay", 150)); + m_ui->triggerCooldownSpin->setValue(config.readEntry("ElectricBorderCooldown", 350)); + m_ui->quickMaximizeBox->setChecked(config.readEntry("ElectricBorderMaximize", true)); + m_ui->quickTileBox->setChecked(config.readEntry("ElectricBorderTiling", true)); + m_ui->electricBorderCornerRatio->setValue(qRound(config.readEntry("ElectricBorderCornerRatio", 0.25)*100)); + + emit changed(false); +} + +void KWinScreenEdgesConfig::save() +{ + KCModule::save(); + + monitorSave(); + + KConfigGroup config(m_config, "Windows"); + + config.writeEntry("ElectricBorders", m_ui->desktopSwitchCombo->currentIndex()); + config.writeEntry("ElectricBorderDelay", m_ui->activationDelaySpin->value()); + config.writeEntry("ElectricBorderCooldown", m_ui->triggerCooldownSpin->value()); + config.writeEntry("ElectricBorderMaximize", m_ui->quickMaximizeBox->isChecked()); + config.writeEntry("ElectricBorderTiling", m_ui->quickTileBox->isChecked()); + config.writeEntry("ElectricBorderCornerRatio", m_ui->electricBorderCornerRatio->value()/100.0); + + config.sync(); + + // 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)); + + emit changed(false); +} + +void KWinScreenEdgesConfig::defaults() +{ + monitorDefaults(); + + m_ui->desktopSwitchCombo->setCurrentIndex(0); + m_ui->activationDelaySpin->setValue(150); + m_ui->triggerCooldownSpin->setValue(350); + m_ui->quickMaximizeBox->setChecked(true); + m_ui->quickTileBox->setChecked(true); + m_ui->electricBorderCornerRatio->setValue(25); + + emit changed(true); +} + +void KWinScreenEdgesConfig::showEvent(QShowEvent* e) +{ + KCModule::showEvent(e); + + monitorShowEvent(); +} + +void KWinScreenEdgesConfig::sanitizeCooldown() +{ + m_ui->triggerCooldownSpin->setMinimum(m_ui->activationDelaySpin->value() + 50); +} + +// 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::monitorAddItem(const QString& item) +{ + for (int i = 0; i < 8; i++) + m_ui->monitor->addEdgeItem(i, item); +} + +void KWinScreenEdgesConfig::monitorItemSetEnabled(int index, bool enabled) +{ + for (int i = 0; i < 8; i++) + m_ui->monitor->setEdgeItemEnabled(i, index, enabled); +} + +void KWinScreenEdgesConfig::monitorInit() +{ + monitorAddItem(i18n("No Action")); + monitorAddItem(i18n("Show Desktop")); + monitorAddItem(i18n("Lock Screen")); + monitorAddItem(i18nc("Open krunner", "Run Command")); + monitorAddItem(i18n("Activity Manager")); + monitorAddItem(i18n("Application Launcher")); + + // Add the effects + const QString presentWindowsName = BuiltInEffects::effectData(BuiltInEffect::PresentWindows).displayName; + monitorAddItem(i18n("%1 - All Desktops", presentWindowsName)); + monitorAddItem(i18n("%1 - Current Desktop", presentWindowsName)); + monitorAddItem(i18n("%1 - Current Application", presentWindowsName)); + monitorAddItem(BuiltInEffects::effectData(BuiltInEffect::DesktopGrid).displayName); + const QString cubeName = BuiltInEffects::effectData(BuiltInEffect::Cube).displayName; + monitorAddItem(i18n("%1 - Cube", cubeName)); + monitorAddItem(i18n("%1 - Cylinder", cubeName)); + monitorAddItem(i18n("%1 - Sphere", cubeName)); + + monitorAddItem(i18n("Toggle window switching")); + 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(); + monitorAddItem(script.name()); + } + + monitorShowEvent(); +} + +void KWinScreenEdgesConfig::monitorLoadAction(ElectricBorder edge, const QString& configName) +{ + KConfigGroup config(m_config, "ElectricBorders"); + QString lowerName = config.readEntry(configName, "None").toLower(); + if (lowerName == "showdesktop") monitorChangeEdge(edge, int(ElectricActionShowDesktop)); + else if (lowerName == "lockscreen") monitorChangeEdge(edge, int(ElectricActionLockScreen)); + else if (lowerName == "krunner") monitorChangeEdge(edge, int(ElectricActionKRunner)); + else if (lowerName == "activitymanager") monitorChangeEdge(edge, int(ElectricActionActivityManager)); + else if (lowerName == "applicationlauncher") monitorChangeEdge(edge, int(ElectricActionApplicationLauncher)); +} + +void KWinScreenEdgesConfig::monitorLoad() +{ + // Load ElectricBorderActions + monitorLoadAction(ElectricTop, "Top"); + monitorLoadAction(ElectricTopRight, "TopRight"); + monitorLoadAction(ElectricRight, "Right"); + monitorLoadAction(ElectricBottomRight, "BottomRight"); + monitorLoadAction(ElectricBottom, "Bottom"); + monitorLoadAction(ElectricBottomLeft, "BottomLeft"); + monitorLoadAction(ElectricLeft, "Left"); + monitorLoadAction(ElectricTopLeft, "TopLeft"); + + // Load effect-specific actions: + + // Present Windows + KConfigGroup presentWindowsConfig(m_config, "Effect-PresentWindows"); + QList list = QList(); + // PresentWindows BorderActivateAll + list.append(int(ElectricTopLeft)); + list = presentWindowsConfig.readEntry("BorderActivateAll", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(PresentWindowsAll)); + } + // PresentWindows BorderActivate + list.clear(); + list.append(int(ElectricNone)); + list = presentWindowsConfig.readEntry("BorderActivate", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(PresentWindowsCurrent)); + } + // PresentWindows BorderActivateClass + list.clear(); + list.append(int(ElectricNone)); + list = presentWindowsConfig.readEntry("BorderActivateClass", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(PresentWindowsClass)); + } + + // Desktop Grid + KConfigGroup gridConfig(m_config, "Effect-DesktopGrid"); + list.clear(); + list.append(int(ElectricNone)); + list = gridConfig.readEntry("BorderActivate", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(DesktopGrid)); + } + + // Desktop Cube + KConfigGroup cubeConfig(m_config, "Effect-Cube"); + list.clear(); + list.append(int(ElectricNone)); + list = cubeConfig.readEntry("BorderActivate", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(Cube)); + } + list.clear(); + list.append(int(ElectricNone)); + list = cubeConfig.readEntry("BorderActivateCylinder", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(Cylinder)); + } + list.clear(); + list.append(int(ElectricNone)); + list = cubeConfig.readEntry("BorderActivateSphere", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(Sphere)); + } + + // TabBox + KConfigGroup tabBoxConfig(m_config, "TabBox"); + list.clear(); + // TabBox + list.append(int(ElectricNone)); + list = tabBoxConfig.readEntry("BorderActivate", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(TabBox)); + } + // Alternative TabBox + list.clear(); + list.append(int(ElectricNone)); + list = tabBoxConfig.readEntry("BorderAlternativeActivate", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(TabBoxAlternative)); + } + + for (int i=0; i < m_scripts.size(); i++) { + int index = EffectCount + i; + KConfigGroup scriptConfig(m_config, "Script-"+m_scripts[i]); + list.append(int(ElectricNone)); + list = scriptConfig.readEntry("BorderActivate", list); + for (int i: list) { + monitorChangeEdge(ElectricBorder(i), index); + } + } +} + +void KWinScreenEdgesConfig::monitorSaveAction(int edge, const QString& configName) +{ + KConfigGroup config(m_config, "ElectricBorders"); + int item = m_ui->monitor->selectedEdgeItem(edge); + if (item == 1) + config.writeEntry(configName, "ShowDesktop"); + else if (item == 2) + config.writeEntry(configName, "LockScreen"); + else if (item == 3) + config.writeEntry(configName, "KRunner"); + else if (item == 4) + config.writeEntry(configName, "ActivityManager"); + else if (item == 5) + config.writeEntry(configName, "ApplicationLauncher"); + else // Anything else + config.writeEntry(configName, "None"); +} + +void KWinScreenEdgesConfig::monitorSave() +{ + // Save ElectricBorderActions + monitorSaveAction(int(Monitor::Top), "Top"); + monitorSaveAction(int(Monitor::TopRight), "TopRight"); + monitorSaveAction(int(Monitor::Right), "Right"); + monitorSaveAction(int(Monitor::BottomRight), "BottomRight"); + monitorSaveAction(int(Monitor::Bottom), "Bottom"); + monitorSaveAction(int(Monitor::BottomLeft), "BottomLeft"); + monitorSaveAction(int(Monitor::Left), "Left"); + monitorSaveAction(int(Monitor::TopLeft), "TopLeft"); + + // Save effect-specific actions: + + // Present Windows + KConfigGroup presentWindowsConfig(m_config, "Effect-PresentWindows"); + presentWindowsConfig.writeEntry("BorderActivateAll", + monitorCheckEffectHasEdge(int(PresentWindowsAll))); + presentWindowsConfig.writeEntry("BorderActivate", + monitorCheckEffectHasEdge(int(PresentWindowsCurrent))); + presentWindowsConfig.writeEntry("BorderActivateClass", + monitorCheckEffectHasEdge(int(PresentWindowsClass))); + + // Desktop Grid + KConfigGroup gridConfig(m_config, "Effect-DesktopGrid"); + gridConfig.writeEntry("BorderActivate", + monitorCheckEffectHasEdge(int(DesktopGrid))); + + // Desktop Cube + KConfigGroup cubeConfig(m_config, "Effect-Cube"); + cubeConfig.writeEntry("BorderActivate", + monitorCheckEffectHasEdge(int(Cube))); + cubeConfig.writeEntry("BorderActivateCylinder", + monitorCheckEffectHasEdge(int(Cylinder))); + cubeConfig.writeEntry("BorderActivateSphere", + monitorCheckEffectHasEdge(int(Sphere))); + + // TabBox + KConfigGroup tabBoxConfig(m_config, "TabBox"); + tabBoxConfig.writeEntry("BorderActivate", + monitorCheckEffectHasEdge(int(TabBox))); + tabBoxConfig.writeEntry("BorderAlternativeActivate", + monitorCheckEffectHasEdge(int(TabBoxAlternative))); + + for (int i=0; i < m_scripts.size(); i++) { + int index = EffectCount + i; + KConfigGroup scriptConfig(m_config, "Script-"+m_scripts[i]); + scriptConfig.writeEntry("BorderActivate", + monitorCheckEffectHasEdge(index)); + } +} + +void KWinScreenEdgesConfig::monitorDefaults() +{ + // Clear all edges + for (int i = 0; i < 8; i++) + m_ui->monitor->selectEdgeItem(i, 0); + + // Present windows = Top-left + m_ui->monitor->selectEdgeItem(int(Monitor::TopLeft), int(PresentWindowsAll)); +} + +void KWinScreenEdgesConfig::monitorShowEvent() +{ + // Check if they are enabled + KConfigGroup config(m_config, "Plugins"); + + // Present Windows + bool enabled = effectEnabled(BuiltInEffect::PresentWindows, config); + monitorItemSetEnabled(int(PresentWindowsCurrent), enabled); + monitorItemSetEnabled(int(PresentWindowsAll), enabled); + + // Desktop Grid + enabled = effectEnabled(BuiltInEffect::DesktopGrid, config); + monitorItemSetEnabled(int(DesktopGrid), enabled); + + // Desktop Cube + enabled = effectEnabled(BuiltInEffect::Cube, config); + monitorItemSetEnabled(int(Cube), enabled); + monitorItemSetEnabled(int(Cylinder), enabled); + monitorItemSetEnabled(int(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"; + monitorItemSetEnabled(int(TabBox), reasonable); + monitorItemSetEnabled(int(TabBoxAlternative), reasonable); +} + +void KWinScreenEdgesConfig::monitorChangeEdge(ElectricBorder border, int index) +{ + switch(border) { + case ElectricTop: + m_ui->monitor->selectEdgeItem(int(Monitor::Top), index); + break; + case ElectricTopRight: + m_ui->monitor->selectEdgeItem(int(Monitor::TopRight), index); + break; + case ElectricRight: + m_ui->monitor->selectEdgeItem(int(Monitor::Right), index); + break; + case ElectricBottomRight: + m_ui->monitor->selectEdgeItem(int(Monitor::BottomRight), index); + break; + case ElectricBottom: + m_ui->monitor->selectEdgeItem(int(Monitor::Bottom), index); + break; + case ElectricBottomLeft: + m_ui->monitor->selectEdgeItem(int(Monitor::BottomLeft), index); + break; + case ElectricLeft: + m_ui->monitor->selectEdgeItem(int(Monitor::Left), index); + break; + case ElectricTopLeft: + m_ui->monitor->selectEdgeItem(int(Monitor::TopLeft), index); + break; + default: // Nothing + break; + } +} + +void KWinScreenEdgesConfig::monitorHideEdge(ElectricBorder border, bool hidden) +{ + switch(border) { + case ElectricTop: + m_ui->monitor->setEdgeHidden(int(Monitor::Top), hidden); + break; + case ElectricTopRight: + m_ui->monitor->setEdgeHidden(int(Monitor::TopRight), hidden); + break; + case ElectricRight: + m_ui->monitor->setEdgeHidden(int(Monitor::Right), hidden); + break; + case ElectricBottomRight: + m_ui->monitor->setEdgeHidden(int(Monitor::BottomRight), hidden); + break; + case ElectricBottom: + m_ui->monitor->setEdgeHidden(int(Monitor::Bottom), hidden); + break; + case ElectricBottomLeft: + m_ui->monitor->setEdgeHidden(int(Monitor::BottomLeft), hidden); + break; + case ElectricLeft: + m_ui->monitor->setEdgeHidden(int(Monitor::Left), hidden); + break; + case ElectricTopLeft: + m_ui->monitor->setEdgeHidden(int(Monitor::TopLeft), hidden); + break; + default: // Nothing + break; + } +} + +QList KWinScreenEdgesConfig::monitorCheckEffectHasEdge(int index) const +{ + QList list = QList(); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::Top)) == index) + list.append(int(ElectricTop)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::TopRight)) == index) + list.append(int(ElectricTopRight)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::Right)) == index) + list.append(int(ElectricRight)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::BottomRight)) == index) + list.append(int(ElectricBottomRight)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::Bottom)) == index) + list.append(int(ElectricBottom)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::BottomLeft)) == index) + list.append(int(ElectricBottomLeft)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::Left)) == index) + list.append(int(ElectricLeft)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::TopLeft)) == index) + list.append(int(ElectricTopLeft)); + + if (list.isEmpty()) + list.append(int(ElectricNone)); + return list; +} + +} // namespace + +#include "main.moc" diff --git a/kcmkwin/kwinscreenedges/main.h b/kcmkwin/kwinscreenedges/main.h new file mode 100644 index 0000000..4b7eb4f --- /dev/null +++ b/kcmkwin/kwinscreenedges/main.h @@ -0,0 +1,98 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +#ifndef __MAIN_H__ +#define __MAIN_H__ + +#include +#include + +#include "kwinglobals.h" + +#include "ui_main.h" + +class QShowEvent; + +namespace KWin +{ +enum class BuiltInEffect; + +class KWinScreenEdgesConfigForm : public QWidget, public Ui::KWinScreenEdgesConfigForm +{ + Q_OBJECT + +public: + explicit KWinScreenEdgesConfigForm(QWidget* parent); +}; + +class KWinScreenEdgesConfig : public KCModule +{ + Q_OBJECT + +public: + explicit KWinScreenEdgesConfig(QWidget* parent, const QVariantList& args); + ~KWinScreenEdgesConfig(); + +public Q_SLOTS: + virtual void groupChanged(); + virtual void save(); + virtual void load(); + virtual void defaults(); +protected: + virtual void showEvent(QShowEvent* e); +private Q_SLOTS: + void sanitizeCooldown(); +private: + KWinScreenEdgesConfigForm* m_ui; + KSharedConfigPtr m_config; + QStringList m_scripts; //list of script IDs ordered in the list they are presented in the menu + + 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 monitorAddItem(const QString& item); + void monitorItemSetEnabled(int index, bool enabled); + void monitorInit(); + void monitorLoadAction(ElectricBorder edge, const QString& configName); + void monitorLoad(); + void monitorSaveAction(int edge, const QString& configName); + void monitorSave(); + void monitorDefaults(); + void monitorShowEvent(); + void monitorChangeEdge(ElectricBorder border, int index); + void monitorHideEdge(ElectricBorder border, bool hidden); + QList monitorCheckEffectHasEdge(int index) const; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwinscreenedges/main.ui b/kcmkwin/kwinscreenedges/main.ui new file mode 100644 index 0000000..b3813f3 --- /dev/null +++ b/kcmkwin/kwinscreenedges/main.ui @@ -0,0 +1,356 @@ + + + KWinScreenEdgesConfigForm + + + + 0 + 0 + 488 + 511 + + + + + + + Active Screen Corners and Edges + + + + + + + 200 + 200 + + + + Qt::StrongFocus + + + + + + + Trigger an action by pushing the mouse cursor against the corresponding screen edge or corner + + + Qt::AlignCenter + + + true + + + + + + + + + + Window Management + + + + + + Maximize windows by dragging them to the top edge of the screen + + + + + + + Tile windows by dragging them to the left or right edges of the screen + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 40 + 20 + + + + + + + + false + + + Quarter tiling triggered in the outer + + + electricBorderCornerRatio + + + + + + + false + + + % + + + 1 + + + 49 + + + + + + + false + + + of the screen + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + true + + + Other Settings + + + + QFormLayout::AllNonFixedFieldsGrow + + + 0 + + + + + Change desktop when the mouse cursor is pushed against the edge of the screen + + + &Switch desktop on edge: + + + desktopSwitchCombo + + + + + + + + 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: + + + activationDelaySpin + + + + + + + ms + + + 1000 + + + 50 + + + 0 + + + + + + + true + + + Amount of time required after triggering an action until the next trigger can occur + + + &Reactivation delay: + + + triggerCooldownSpin + + + + + + + true + + + ms + + + 1000 + + + 50 + + + 0 + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + KComboBox + QComboBox +
kcombobox.h
+
+ + KWin::Monitor + QWidget +
monitor.h
+ 1 +
+
+ + monitor + desktopSwitchCombo + activationDelaySpin + triggerCooldownSpin + + + + + quickTileBox + toggled(bool) + label_3 + setEnabled(bool) + + + 93 + 306 + + + 105 + 329 + + + + + quickTileBox + toggled(bool) + electricBorderCornerRatio + setEnabled(bool) + + + 164 + 312 + + + 301 + 345 + + + + + quickTileBox + toggled(bool) + label_4 + setEnabled(bool) + + + 220 + 305 + + + 340 + 329 + + + + +
diff --git a/kcmkwin/kwinscreenedges/monitor.cpp b/kcmkwin/kwinscreenedges/monitor.cpp new file mode 100644 index 0000000..543800d --- /dev/null +++ b/kcmkwin/kwinscreenedges/monitor.cpp @@ -0,0 +1,292 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2008 Lubos Lunak +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +#include "monitor.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace KWin +{ + +Monitor::Monitor(QWidget* parent) + : ScreenPreviewWidget(parent) +{ + QDesktopWidget *desktop = QApplication::desktop(); + QRect avail = desktop->availableGeometry(desktop->screenNumber(this)); + 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::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..919937d --- /dev/null +++ b/kcmkwin/kwinscreenedges/monitor.h @@ -0,0 +1,113 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2008 Lubos Lunak +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +#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 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 + }; +Q_SIGNALS: + void changed(); + void edgeSelectionChanged(int edge, int index); +protected: + virtual void resizeEvent(QResizeEvent* e); +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(); + void setActive(bool active); + bool active() const; +protected: + virtual void contextMenuEvent(QGraphicsSceneContextMenuEvent* e); + virtual void mousePressEvent(QGraphicsSceneMouseEvent* e); + virtual void hoverEnterEvent(QGraphicsSceneHoverEvent * e); + virtual void hoverLeaveEvent(QGraphicsSceneHoverEvent * e); + virtual void paint(QPainter* painter, const QStyleOptionGraphicsItem* option, QWidget* widget = 0); +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..5fc429f --- /dev/null +++ b/kcmkwin/kwinscreenedges/screenpreviewwidget.cpp @@ -0,0 +1,163 @@ +/* This file is part of the KDE libraries + + Copyright (C) 2009 Marco Martin + + 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; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#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..470d314 --- /dev/null +++ b/kcmkwin/kwinscreenedges/screenpreviewwidget.h @@ -0,0 +1,58 @@ +/* This file is part of the KDE libraries + + Copyright (C) 2009 Marco Martin + + 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; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef SCREENPREVIEWWIDGET_H +#define SCREENPREVIEWWIDGET_H + +#include + +class ScreenPreviewWidgetPrivate; + +class ScreenPreviewWidget : public QWidget +{ + Q_OBJECT + +public: + ScreenPreviewWidget(QWidget *parent); + ~ScreenPreviewWidget(); + + 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); + void paintEvent(QPaintEvent *event); + virtual void dropEvent(QDropEvent *event); + +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..96793fd --- /dev/null +++ b/kcmkwin/kwinscreenedges/touch.cpp @@ -0,0 +1,472 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2008 Martin Gräßlin +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +#include "touch.h" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +K_PLUGIN_FACTORY(KWinScreenEdgesConfigFactory, registerPlugin();) + +namespace KWin +{ + +KWinScreenEdgesConfigForm::KWinScreenEdgesConfigForm(QWidget* parent) + : QWidget(parent) +{ + setupUi(this); +} + +KWinScreenEdgesConfig::KWinScreenEdgesConfig(QWidget* parent, const QVariantList& args) + : KCModule(parent, args) + , m_config(KSharedConfig::openConfig("kwinrc")) +{ + m_ui = new KWinScreenEdgesConfigForm(this); + QVBoxLayout* layout = new QVBoxLayout(this); + layout->addWidget(m_ui); + + monitorInit(); + + connect(m_ui->monitor, SIGNAL(changed()), this, SLOT(changed())); + + load(); +} + +KWinScreenEdgesConfig::~KWinScreenEdgesConfig() +{ +} + +void KWinScreenEdgesConfig::load() +{ + KCModule::load(); + + monitorLoad(); + + emit changed(false); +} + +void KWinScreenEdgesConfig::save() +{ + KCModule::save(); + + monitorSave(); + + // 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)); + + emit changed(false); +} + +void KWinScreenEdgesConfig::defaults() +{ + monitorDefaults(); + + emit changed(true); +} + +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::monitorAddItem(const QString& item) +{ + for (int i = 0; i < 8; i++) + m_ui->monitor->addEdgeItem(i, item); +} + +void KWinScreenEdgesConfig::monitorItemSetEnabled(int index, bool enabled) +{ + for (int i = 0; i < 8; i++) + m_ui->monitor->setEdgeItemEnabled(i, index, enabled); +} + +void KWinScreenEdgesConfig::monitorInit() +{ + monitorAddItem(i18n("No Action")); + monitorAddItem(i18n("Show Desktop")); + monitorAddItem(i18n("Lock Screen")); + monitorAddItem(i18nc("Open krunner", "Run Command")); + monitorAddItem(i18n("Activity Manager")); + monitorAddItem(i18n("Application Launcher")); + + // Add the effects + const QString presentWindowsName = BuiltInEffects::effectData(BuiltInEffect::PresentWindows).displayName; + monitorAddItem(i18n("%1 - All Desktops", presentWindowsName)); + monitorAddItem(i18n("%1 - Current Desktop", presentWindowsName)); + monitorAddItem(i18n("%1 - Current Application", presentWindowsName)); + monitorAddItem(BuiltInEffects::effectData(BuiltInEffect::DesktopGrid).displayName); + const QString cubeName = BuiltInEffects::effectData(BuiltInEffect::Cube).displayName; + monitorAddItem(i18n("%1 - Cube", cubeName)); + monitorAddItem(i18n("%1 - Cylinder", cubeName)); + monitorAddItem(i18n("%1 - Sphere", cubeName)); + + monitorAddItem(i18n("Toggle window switching")); + 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(); + monitorAddItem(script.name()); + } + + monitorHideEdge(ElectricTopLeft, true); + monitorHideEdge(ElectricTopRight, true); + monitorHideEdge(ElectricBottomRight, true); + monitorHideEdge(ElectricBottomLeft, true); + + monitorShowEvent(); +} + +void KWinScreenEdgesConfig::monitorLoadAction(ElectricBorder edge, const QString& configName) +{ + KConfigGroup config(m_config, "TouchEdges"); + QString lowerName = config.readEntry(configName, "None").toLower(); + if (lowerName == "showdesktop") monitorChangeEdge(edge, int(ElectricActionShowDesktop)); + else if (lowerName == "lockscreen") monitorChangeEdge(edge, int(ElectricActionLockScreen)); + else if (lowerName == "krunner") monitorChangeEdge(edge, int(ElectricActionKRunner)); + else if (lowerName == "activitymanager") monitorChangeEdge(edge, int(ElectricActionActivityManager)); + else if (lowerName == "applicationlauncher") monitorChangeEdge(edge, int(ElectricActionApplicationLauncher)); +} + +void KWinScreenEdgesConfig::monitorLoad() +{ + // Load ElectricBorderActions + monitorLoadAction(ElectricTop, "Top"); + monitorLoadAction(ElectricRight, "Right"); + monitorLoadAction(ElectricBottom, "Bottom"); + monitorLoadAction(ElectricLeft, "Left"); + + // Load effect-specific actions: + + // Present Windows + KConfigGroup presentWindowsConfig(m_config, "Effect-PresentWindows"); + QList list = QList(); + // PresentWindows BorderActivateAll + list.append(int(ElectricTopLeft)); + list = presentWindowsConfig.readEntry("TouchBorderActivateAll", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(PresentWindowsAll)); + } + // PresentWindows BorderActivate + list.clear(); + list.append(int(ElectricNone)); + list = presentWindowsConfig.readEntry("TouchBorderActivate", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(PresentWindowsCurrent)); + } + // PresentWindows BorderActivateClass + list.clear(); + list.append(int(ElectricNone)); + list = presentWindowsConfig.readEntry("TouchBorderActivateClass", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(PresentWindowsClass)); + } + + // Desktop Grid + KConfigGroup gridConfig(m_config, "Effect-DesktopGrid"); + list.clear(); + list.append(int(ElectricNone)); + list = gridConfig.readEntry("TouchBorderActivate", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(DesktopGrid)); + } + + // Desktop Cube + KConfigGroup cubeConfig(m_config, "Effect-Cube"); + list.clear(); + list.append(int(ElectricNone)); + list = cubeConfig.readEntry("TouchBorderActivate", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(Cube)); + } + list.clear(); + list.append(int(ElectricNone)); + list = cubeConfig.readEntry("TouchBorderActivateCylinder", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(Cylinder)); + } + list.clear(); + list.append(int(ElectricNone)); + list = cubeConfig.readEntry("TouchBorderActivateSphere", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(Sphere)); + } + + // TabBox + KConfigGroup tabBoxConfig(m_config, "TabBox"); + list.clear(); + // TabBox + list.append(int(ElectricLeft)); + list = tabBoxConfig.readEntry("TouchBorderActivate", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(TabBox)); + } + // Alternative TabBox + list.clear(); + list.append(int(ElectricNone)); + list = tabBoxConfig.readEntry("TouchBorderAlternativeActivate", list); + foreach (int i, list) { + monitorChangeEdge(ElectricBorder(i), int(TabBoxAlternative)); + } + + for (int i=0; i < m_scripts.size(); i++) { + int index = EffectCount + i; + KConfigGroup scriptConfig(m_config, "Script-"+m_scripts[i]); + list.append(int(ElectricNone)); + list = scriptConfig.readEntry("TouchBorderActivate", list); + for (int i: list) { + monitorChangeEdge(ElectricBorder(i), index); + } + } +} + +void KWinScreenEdgesConfig::monitorSaveAction(int edge, const QString& configName) +{ + KConfigGroup config(m_config, "TouchEdges"); + int item = m_ui->monitor->selectedEdgeItem(edge); + if (item == 1) + config.writeEntry(configName, "ShowDesktop"); + else if (item == 2) + config.writeEntry(configName, "LockScreen"); + else if (item == 3) + config.writeEntry(configName, "KRunner"); + else if (item == 4) + config.writeEntry(configName, "ActivityManager"); + else if (item == 5) + config.writeEntry(configName, "ApplicationLauncher"); + else // Anything else + config.writeEntry(configName, "None"); +} + +void KWinScreenEdgesConfig::monitorSave() +{ + // Save ElectricBorderActions + monitorSaveAction(int(Monitor::Top), "Top"); + monitorSaveAction(int(Monitor::Right), "Right"); + monitorSaveAction(int(Monitor::Bottom), "Bottom"); + monitorSaveAction(int(Monitor::Left), "Left"); + + // Save effect-specific actions: + + // Present Windows + KConfigGroup presentWindowsConfig(m_config, "Effect-PresentWindows"); + presentWindowsConfig.writeEntry("TouchBorderActivate", + monitorCheckEffectHasEdge(int(PresentWindowsAll))); + presentWindowsConfig.writeEntry("TouchBorderActivateAll", + monitorCheckEffectHasEdge(int(PresentWindowsCurrent))); + presentWindowsConfig.writeEntry("TouchBorderActivateClass", + monitorCheckEffectHasEdge(int(PresentWindowsClass))); + + // Desktop Grid + KConfigGroup gridConfig(m_config, "Effect-DesktopGrid"); + gridConfig.writeEntry("TouchBorderActivate", + monitorCheckEffectHasEdge(int(DesktopGrid))); + + // Desktop Cube + KConfigGroup cubeConfig(m_config, "Effect-Cube"); + cubeConfig.writeEntry("TouchBorderActivate", + monitorCheckEffectHasEdge(int(Cube))); + cubeConfig.writeEntry("TouchBorderActivateCylinder", + monitorCheckEffectHasEdge(int(Cylinder))); + cubeConfig.writeEntry("TouchBorderActivateSphere", + monitorCheckEffectHasEdge(int(Sphere))); + + // TabBox + KConfigGroup tabBoxConfig(m_config, "TabBox"); + tabBoxConfig.writeEntry("TouchBorderActivate", + monitorCheckEffectHasEdge(int(TabBox))); + tabBoxConfig.writeEntry("TouchBorderAlternativeActivate", + monitorCheckEffectHasEdge(int(TabBoxAlternative))); + + for (int i=0; i < m_scripts.size(); i++) { + int index = EffectCount + i; + KConfigGroup scriptConfig(m_config, "Script-"+m_scripts[i]); + scriptConfig.writeEntry("TouchBorderActivate", + monitorCheckEffectHasEdge(index)); + } +} + +void KWinScreenEdgesConfig::monitorDefaults() +{ + // Clear all edges + for (int i = 0; i < 8; i++) + m_ui->monitor->selectEdgeItem(i, 0); + // select TabBox + m_ui->monitor->selectEdgeItem(int(Monitor::Left), int(TabBox)); +} + +void KWinScreenEdgesConfig::monitorShowEvent() +{ + // Check if they are enabled + KConfigGroup config(m_config, "Plugins"); + + // Present Windows + bool enabled = effectEnabled(BuiltInEffect::PresentWindows, config); + monitorItemSetEnabled(int(PresentWindowsCurrent), enabled); + monitorItemSetEnabled(int(PresentWindowsAll), enabled); + + // Desktop Grid + enabled = effectEnabled(BuiltInEffect::DesktopGrid, config); + monitorItemSetEnabled(int(DesktopGrid), enabled); + + // Desktop Cube + enabled = effectEnabled(BuiltInEffect::Cube, config); + monitorItemSetEnabled(int(Cube), enabled); + monitorItemSetEnabled(int(Cylinder), enabled); + monitorItemSetEnabled(int(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"; + monitorItemSetEnabled(int(TabBox), reasonable); + monitorItemSetEnabled(int(TabBoxAlternative), reasonable); +} + +void KWinScreenEdgesConfig::monitorChangeEdge(ElectricBorder border, int index) +{ + switch(border) { + case ElectricTop: + m_ui->monitor->selectEdgeItem(int(Monitor::Top), index); + break; + case ElectricTopRight: + m_ui->monitor->selectEdgeItem(int(Monitor::TopRight), index); + break; + case ElectricRight: + m_ui->monitor->selectEdgeItem(int(Monitor::Right), index); + break; + case ElectricBottomRight: + m_ui->monitor->selectEdgeItem(int(Monitor::BottomRight), index); + break; + case ElectricBottom: + m_ui->monitor->selectEdgeItem(int(Monitor::Bottom), index); + break; + case ElectricBottomLeft: + m_ui->monitor->selectEdgeItem(int(Monitor::BottomLeft), index); + break; + case ElectricLeft: + m_ui->monitor->selectEdgeItem(int(Monitor::Left), index); + break; + case ElectricTopLeft: + m_ui->monitor->selectEdgeItem(int(Monitor::TopLeft), index); + break; + default: // Nothing + break; + } +} + +void KWinScreenEdgesConfig::monitorHideEdge(ElectricBorder border, bool hidden) +{ + switch(border) { + case ElectricTop: + m_ui->monitor->setEdgeHidden(int(Monitor::Top), hidden); + break; + case ElectricTopRight: + m_ui->monitor->setEdgeHidden(int(Monitor::TopRight), hidden); + break; + case ElectricRight: + m_ui->monitor->setEdgeHidden(int(Monitor::Right), hidden); + break; + case ElectricBottomRight: + m_ui->monitor->setEdgeHidden(int(Monitor::BottomRight), hidden); + break; + case ElectricBottom: + m_ui->monitor->setEdgeHidden(int(Monitor::Bottom), hidden); + break; + case ElectricBottomLeft: + m_ui->monitor->setEdgeHidden(int(Monitor::BottomLeft), hidden); + break; + case ElectricLeft: + m_ui->monitor->setEdgeHidden(int(Monitor::Left), hidden); + break; + case ElectricTopLeft: + m_ui->monitor->setEdgeHidden(int(Monitor::TopLeft), hidden); + break; + default: // Nothing + break; + } +} + +QList KWinScreenEdgesConfig::monitorCheckEffectHasEdge(int index) const +{ + QList list = QList(); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::Top)) == index) + list.append(int(ElectricTop)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::TopRight)) == index) + list.append(int(ElectricTopRight)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::Right)) == index) + list.append(int(ElectricRight)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::BottomRight)) == index) + list.append(int(ElectricBottomRight)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::Bottom)) == index) + list.append(int(ElectricBottom)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::BottomLeft)) == index) + list.append(int(ElectricBottomLeft)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::Left)) == index) + list.append(int(ElectricLeft)); + if (m_ui->monitor->selectedEdgeItem(int(Monitor::TopLeft)) == index) + list.append(int(ElectricTopLeft)); + + if (list.isEmpty()) + list.append(int(ElectricNone)); + return list; +} + +} // namespace + +#include "touch.moc" diff --git a/kcmkwin/kwinscreenedges/touch.h b/kcmkwin/kwinscreenedges/touch.h new file mode 100644 index 0000000..8fc61e3 --- /dev/null +++ b/kcmkwin/kwinscreenedges/touch.h @@ -0,0 +1,95 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ + +#ifndef __TOUCH_H__ +#define __TOUCH_H__ + +#include +#include + +#include "kwinglobals.h" + +#include "ui_touch.h" + +class QShowEvent; + +namespace KWin +{ +enum class BuiltInEffect; + +class KWinScreenEdgesConfigForm : public QWidget, public Ui::KWinScreenEdgesConfigForm +{ + Q_OBJECT + +public: + explicit KWinScreenEdgesConfigForm(QWidget* parent); +}; + +class KWinScreenEdgesConfig : public KCModule +{ + Q_OBJECT + +public: + explicit KWinScreenEdgesConfig(QWidget* parent, const QVariantList& args); + ~KWinScreenEdgesConfig(); + +public Q_SLOTS: + virtual void save(); + virtual void load(); + virtual void defaults(); +protected: + virtual void showEvent(QShowEvent* e); +private: + KWinScreenEdgesConfigForm* m_ui; + KSharedConfigPtr m_config; + QStringList m_scripts; //list of script IDs ordered in the list they are presented in the menu + + 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 monitorAddItem(const QString& item); + void monitorItemSetEnabled(int index, bool enabled); + void monitorInit(); + void monitorLoadAction(ElectricBorder edge, const QString& configName); + void monitorLoad(); + void monitorSaveAction(int edge, const QString& configName); + void monitorSave(); + void monitorDefaults(); + void monitorShowEvent(); + void monitorChangeEdge(ElectricBorder border, int index); + void monitorHideEdge(ElectricBorder border, bool hidden); + QList monitorCheckEffectHasEdge(int index) const; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwinscreenedges/touch.ui b/kcmkwin/kwinscreenedges/touch.ui new file mode 100644 index 0000000..997989b --- /dev/null +++ b/kcmkwin/kwinscreenedges/touch.ui @@ -0,0 +1,71 @@ + + + KWinScreenEdgesConfigForm + + + + 0 + 0 + 748 + 332 + + + + + + + + 0 + 0 + + + + + 200 + 200 + + + + Qt::StrongFocus + + + + + + + Trigger an action by swiping from the screen edge towards the center of the screen + + + Qt::AlignCenter + + + true + + + + + + + Qt::Vertical + + + + 20 + 0 + + + + + + + + + KWin::Monitor + QWidget +
monitor.h
+ 1 +
+
+ + +
diff --git a/kcmkwin/kwinscripts/CMakeLists.txt b/kcmkwin/kwinscripts/CMakeLists.txt new file mode 100644 index 0000000..e21879b --- /dev/null +++ b/kcmkwin/kwinscripts/CMakeLists.txt @@ -0,0 +1,27 @@ +# 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::KCMUtils + KF5::KIOCore + KF5::I18n + KF5::Package + KF5::NewStuff +) + +install(TARGETS kcm_kwin_scripts DESTINATION ${PLUGIN_INSTALL_DIR}) +install(FILES kwinscripts.desktop DESTINATION ${SERVICES_INSTALL_DIR}) +install(FILES kwinscripts.knsrc DESTINATION ${CONFIG_INSTALL_DIR}) 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..7d975c1 --- /dev/null +++ b/kcmkwin/kwinscripts/kwinscripts.desktop @@ -0,0 +1,163 @@ +[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[bs]=kwin skripta +X-KDE-Keywords[ca]=script del kwin +X-KDE-Keywords[ca@valencia]=script del 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[bs]=KWin skripte +Name[ca]=Scripts del KWin +Name[ca@valencia]=Scripts del 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]=KWin Scripts +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[bs]=Podesi KWin skripte +Comment[ca]=Gestiona els scripts del KWin +Comment[ca@valencia]=Gestiona els scripts del 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]=Valdyti 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..d5f9d1b --- /dev/null +++ b/kcmkwin/kwinscripts/kwinscripts.knsrc @@ -0,0 +1,45 @@ +[KNewStuff3] +Name=Window Manager Scripts +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[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]=Window Manager Scripts +Name[it]=Script del gestore delle finestre +Name[ko]=창 관리자 스크립트 +Name[lt]=Langų tvarkyklė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[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 +InstallationCommand=kpackagetool5 --type KWin/Script --install %f +UninstallCommand=kpackagetool5 --type KWin/Script --remove %f diff --git a/kcmkwin/kwinscripts/main.cpp b/kcmkwin/kwinscripts/main.cpp new file mode 100644 index 0000000..baa5175 --- /dev/null +++ b/kcmkwin/kwinscripts/main.cpp @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2011 Tamas Krutki + * + * 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. + */ + +#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..e1eb28c --- /dev/null +++ b/kcmkwin/kwinscripts/module.cpp @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2011 Tamas Krutki + * Copyright (c) 2012 Martin Gräßlin + * + * 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. + */ + +#include "module.h" +#include "ui_module.h" + +#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()) { + updateListViewContents(); + } + }); + + connect(ui->scriptSelector, SIGNAL(changed(bool)), this, SLOT(changed())); + connect(ui->importScriptButton, SIGNAL(clicked()), SLOT(importScript())); + + 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) { + if (md.value(QStringLiteral("X-KWin-Exclude-Listing")) == QLatin1String("true") ) { + return false; + } + return true; + }; + + 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(); + emit changed(true); +} + +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..a7017d3 --- /dev/null +++ b/kcmkwin/kwinscripts/module.h @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2011 Tamas Krutki + * + * 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. + */ + +#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(); + virtual void load(); + virtual void save(); + virtual void defaults(); + +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..60ef9a8 --- /dev/null +++ b/kcmkwin/kwinscripts/module.ui @@ -0,0 +1,95 @@ + + + Module + + + + 0 + 0 + 484 + 300 + + + + KWin script configuration + + + + + + + + + + + + + + 0 + 0 + + + + Qt::WheelFocus + + + + + + + + + + + Import KWin script... + + + + + + + Get New Scripts... + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + 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..b4d5102 --- /dev/null +++ b/kcmkwin/kwintabbox/CMakeLists.txt @@ -0,0 +1,40 @@ +# 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 + main.cpp + layoutpreview.cpp + thumbnailitem.cpp + ${KWIN_SOURCE_DIR}/tabbox/tabboxconfig.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) + +add_library(kcm_kwintabbox MODULE ${kcm_kwintabbox_PART_SRCS}) + +target_link_libraries(kcm_kwintabbox + Qt5::Quick + KF5::KCMUtils + KF5::Completion + KF5::GlobalAccel + KF5::I18n + KF5::Service + KF5::NewStuff + KF5::Package + 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 ${CONFIG_INSTALL_DIR} ) 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/kwinswitcher.knsrc b/kcmkwin/kwintabbox/kwinswitcher.knsrc new file mode 100644 index 0000000..5add52e --- /dev/null +++ b/kcmkwin/kwintabbox/kwinswitcher.knsrc @@ -0,0 +1,46 @@ +[KNewStuff3] +Name=Window Manager Switching Layouts +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[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]=Window Manager Switching Layouts +Name[it]=Disposizione scambiafinestre del gestore delle finestre +Name[ko]=창 관리자 전환기 레이아웃 +Name[lt]=Langų 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[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 +InstallationCommand=kpackagetool5 --type KWin/WindowSwitcher --install %f +UninstallCommand=kpackagetool5 --type KWin/WindowSwitcher --remove %f diff --git a/kcmkwin/kwintabbox/kwintabbox.desktop b/kcmkwin/kwintabbox/kwintabbox.desktop new file mode 100644 index 0000000..f463185 --- /dev/null +++ b/kcmkwin/kwintabbox/kwintabbox.desktop @@ -0,0 +1,156 @@ +[Desktop Entry] +Type=Service +X-KDE-ServiceTypes=KCModule +Icon=window-duplicate +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[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]=Task Switcher +Name[is]=Verkefnaskiptir +Name[it]=Scambiafinestre +Name[ja]=タスクスイッチャー +Name[kk]=Тапсырма ауыстырғышы +Name[km]=កម្មវិធី​ប្ដូរ​ភារកិច្ច​ +Name[kn]=ಕಾರ್ಯ ಬದಲಾವಣೆಗಾರ +Name[ko]=작업 전환기 +Name[lt]=Užduočių keitiklis +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[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[id]=Navigasi Melalui Jendela +Comment[it]=Navigazione tra le finestre +Comment[ko]=창간 탐색 +Comment[lt]=Judėjimas 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[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[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]=jendela,jendela,pengalih,pengalih jendela,pengalihan,pengalihan jendela,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[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[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/layoutpreview.cpp b/kcmkwin/kwintabbox/layoutpreview.cpp new file mode 100644 index 0000000..f63a756 --- /dev/null +++ b/kcmkwin/kwintabbox/layoutpreview.cpp @@ -0,0 +1,260 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ +// 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) +{ + QHash roles; + roles[Qt::UserRole] = "caption"; + roles[Qt::UserRole+1] = "minimized"; + roles[Qt::UserRole + 3] = "icon"; + roles[Qt::UserRole+2] = "desktopName"; + roles[Qt::UserRole+4] = "windowId"; + setRoleNames(roles); + init(); +} + +ExampleClientModel::~ExampleClientModel() +{ +} + +void ExampleClientModel::init() +{ + if (const auto s = KMimeTypeTrader::self()->preferredService(QStringLiteral("inode/directory"))) { + m_services << s; + m_fileManager = s; + } + if (const auto s = KMimeTypeTrader::self()->preferredService(QStringLiteral("text/html"))) { + m_services << s; + m_browser = s; + } + if (const auto s = KMimeTypeTrader::self()->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 Qt::UserRole: + return m_services.at(index.row())->name(); + case Qt::UserRole+1: + return false; + case Qt::UserRole+2: + return i18nc("An example Desktop Name", "Desktop 1"); + case Qt::UserRole+3: + return m_services.at(index.row())->icon(); + case Qt::UserRole+4: + 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(); +} + +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 +{ + return qApp->desktop()->screenGeometry(qApp->desktop()->primaryScreen()); +} + +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..9a7d65e --- /dev/null +++ b/kcmkwin/kwintabbox/layoutpreview.h @@ -0,0 +1,144 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~LayoutPreview(); + + virtual bool eventFilter(QObject *object, QEvent *event) override; +private: + SwitcherItem *m_item; +}; + +class ExampleClientModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit ExampleClientModel(QObject *parent = nullptr); + virtual ~ExampleClientModel(); + + virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; + virtual int rowCount(const QModelIndex &parent = QModelIndex()) const; + 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); + virtual ~SwitcherItem(); + + 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..82e72fe --- /dev/null +++ b/kcmkwin/kwintabbox/main.cpp @@ -0,0 +1,592 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "main.h" +#include +#include + +// Qt +#include +#include +#include +#include +#include +#include + +// KDE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +// Plasma +#include +#include + +// own +#include "tabboxconfig.h" +#include "layoutpreview.h" + +K_PLUGIN_FACTORY(KWinTabBoxConfigFactory, registerPlugin();) + +namespace KWin +{ + +using namespace TabBox; + +KWinTabBoxConfigForm::KWinTabBoxConfigForm(QWidget* parent) + : QWidget(parent) +{ + setupUi(this); +} + +KWinTabBoxConfig::KWinTabBoxConfig(QWidget* parent, const QVariantList& args) + : KCModule(parent, args) + , m_config(KSharedConfig::openConfig("kwinrc")) +{ + QTabWidget* tabWidget = new QTabWidget(this); + m_primaryTabBoxUi = new KWinTabBoxConfigForm(tabWidget); + m_alternativeTabBoxUi = new KWinTabBoxConfigForm(tabWidget); + tabWidget->addTab(m_primaryTabBoxUi, i18n("Main")); + tabWidget->addTab(m_alternativeTabBoxUi, i18n("Alternative")); + 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->setPixmap(KTitleWidget::InfoMessage, KTitleWidget::ImageLeft); + layout->addWidget(infoLabel,0); + layout->addWidget(tabWidget,1); + setLayout(layout); + +#define ADD_SHORTCUT(_NAME_, _CUT_, _BTN_) \ + a = m_actionCollection->addAction(_NAME_);\ + a->setProperty("isConfigurationAction", true);\ + _BTN_->setProperty("shortcutAction", _NAME_);\ + a->setText(i18n(_NAME_));\ + KGlobalAccel::self()->setShortcut(a, QList() << _CUT_); \ + connect(_BTN_, SIGNAL(keySequenceChanged(QKeySequence)), SLOT(shortcutChanged(QKeySequence))) + + // Shortcut config. The shortcut belongs to the component "kwin"! + m_actionCollection = new KActionCollection(this, QStringLiteral("kwin")); + m_actionCollection->setConfigGroup("Navigation"); + m_actionCollection->setConfigGlobal(true); + QAction* a; + ADD_SHORTCUT("Walk Through Windows", Qt::ALT + Qt::Key_Tab, m_primaryTabBoxUi->scAll); + ADD_SHORTCUT("Walk Through Windows (Reverse)", Qt::ALT + Qt::SHIFT + Qt::Key_Backtab, + m_primaryTabBoxUi->scAllReverse); + ADD_SHORTCUT("Walk Through Windows Alternative", QKeySequence(), m_alternativeTabBoxUi->scAll); + ADD_SHORTCUT("Walk Through Windows Alternative (Reverse)", QKeySequence(), m_alternativeTabBoxUi->scAllReverse); + ADD_SHORTCUT("Walk Through Windows of Current Application", Qt::ALT + Qt::Key_QuoteLeft, + m_primaryTabBoxUi->scCurrent); + ADD_SHORTCUT("Walk Through Windows of Current Application (Reverse)", Qt::ALT + Qt::Key_AsciiTilde, + m_primaryTabBoxUi->scCurrentReverse); + ADD_SHORTCUT("Walk Through Windows of Current Application Alternative", QKeySequence(), m_alternativeTabBoxUi->scCurrent); + ADD_SHORTCUT("Walk Through Windows of Current Application Alternative (Reverse)", QKeySequence(), + m_alternativeTabBoxUi->scCurrentReverse); +#undef ADD_SHORTCUT + + initLayoutLists(); + KWinTabBoxConfigForm *ui[2] = { m_primaryTabBoxUi, m_alternativeTabBoxUi }; + for (int i = 0; i < 2; ++i) { + ui[i]->effectConfigButton->setIcon(QIcon::fromTheme(QStringLiteral("view-preview"))); + ui[i]->ghns->setIcon(QIcon::fromTheme(QStringLiteral("get-hot-new-stuff"))); + + connect(ui[i]->highlightWindowCheck, SIGNAL(clicked(bool)), SLOT(changed())); + connect(ui[i]->showTabBox, SIGNAL(clicked(bool)), SLOT(tabBoxToggled(bool))); + connect(ui[i]->effectCombo, SIGNAL(currentIndexChanged(int)), SLOT(changed())); + connect(ui[i]->effectCombo, SIGNAL(currentIndexChanged(int)), SLOT(effectSelectionChanged(int))); + connect(ui[i]->effectConfigButton, SIGNAL(clicked(bool)), SLOT(configureEffectClicked())); + + connect(ui[i]->switchingModeCombo, SIGNAL(currentIndexChanged(int)), SLOT(changed())); + connect(ui[i]->showDesktop, SIGNAL(clicked(bool)), SLOT(changed())); + + connect(ui[i]->filterDesktops, SIGNAL(clicked(bool)), SLOT(changed())); + connect(ui[i]->currentDesktop, SIGNAL(clicked(bool)), SLOT(changed())); + connect(ui[i]->otherDesktops, SIGNAL(clicked(bool)), SLOT(changed())); + + connect(ui[i]->filterActivities, SIGNAL(clicked(bool)), SLOT(changed())); + connect(ui[i]->currentActivity, SIGNAL(clicked(bool)), SLOT(changed())); + connect(ui[i]->otherActivities, SIGNAL(clicked(bool)), SLOT(changed())); + + connect(ui[i]->filterScreens, SIGNAL(clicked(bool)), SLOT(changed())); + if (QApplication::desktop()->screenCount() < 2) { + ui[i]->filterScreens->hide(); + ui[i]->screenFilter->hide(); + } else { + connect(ui[i]->currentScreen, SIGNAL(clicked(bool)), SLOT(changed())); + connect(ui[i]->otherScreens, SIGNAL(clicked(bool)), SLOT(changed())); + } + + connect(ui[i]->oneAppWindow, SIGNAL(clicked(bool)), SLOT(changed())); + connect(ui[i]->filterMinimization, SIGNAL(clicked(bool)), SLOT(changed())); + connect(ui[i]->visibleWindows, SIGNAL(clicked(bool)), SLOT(changed())); + connect(ui[i]->hiddenWindows, SIGNAL(clicked(bool)), SLOT(changed())); + connect(ui[i]->ghns, SIGNAL(clicked(bool)), SLOT(slotGHNS())); + } + + // 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(); +} + +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 + QString coverswitch = BuiltInEffects::effectData(BuiltInEffect::CoverSwitch).displayName; + QString flipswitch = BuiltInEffects::effectData(BuiltInEffect::FlipSwitch).displayName; + + 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) { + int index = ui[i]->effectCombo->currentIndex(); + QVariant data = ui[i]->effectCombo->itemData(index); + ui[i]->effectCombo->clear(); + ui[i]->effectCombo->addItem(coverswitch); + ui[i]->effectCombo->addItem(flipswitch); + for (int j = 0; j < layoutNames.count(); ++j) { + ui[i]->effectCombo->addItem(layoutNames[j], layoutPlugins[j]); + ui[i]->effectCombo->setItemData(ui[i]->effectCombo->count() - 1, layoutPaths[j], Qt::UserRole+1); + } + if (data.isValid()) { + ui[i]->effectCombo->setCurrentIndex(ui[i]->effectCombo->findData(data)); + } else if (index != -1) { + ui[i]->effectCombo->setCurrentIndex(index); + } + } +} + +void KWinTabBoxConfig::load() +{ + KCModule::load(); + + const QString group[2] = { "TabBox", "TabBoxAlternative" }; + KWinTabBoxConfigForm* ui[2] = { m_primaryTabBoxUi, m_alternativeTabBoxUi }; + TabBoxConfig *tabBoxConfig[2] = { &m_tabBoxConfig, &m_tabBoxAlternativeConfig }; + + for (int i = 0; i < 2; ++i) { + KConfigGroup config(m_config, group[i]); + loadConfig(config, *(tabBoxConfig[i])); + + updateUiFromConfig(ui[i], *(tabBoxConfig[i])); + + KConfigGroup effectconfig(m_config, "Plugins"); + if (effectEnabled(BuiltInEffect::CoverSwitch, effectconfig) && KConfigGroup(m_config, "Effect-CoverSwitch").readEntry(group[i], false)) + ui[i]->effectCombo->setCurrentIndex(CoverSwitch); + else if (effectEnabled(BuiltInEffect::FlipSwitch, effectconfig) && KConfigGroup(m_config, "Effect-FlipSwitch").readEntry(group[i], false)) + ui[i]->effectCombo->setCurrentIndex(FlipSwitch); + + QString action; +#define LOAD_SHORTCUT(_BTN_)\ + action = ui[i]->_BTN_->property("shortcutAction").toString();\ + qDebug() << "load shortcut for " << action;\ + if (QAction *a = m_actionCollection->action(action)) { \ + auto shortcuts = KGlobalAccel::self()->shortcut(a); \ + if (!shortcuts.isEmpty()) \ + ui[i]->_BTN_->setKeySequence(shortcuts.first()); \ + } + LOAD_SHORTCUT(scAll); + LOAD_SHORTCUT(scAllReverse); + LOAD_SHORTCUT(scCurrent); + LOAD_SHORTCUT(scCurrentReverse); +#undef LOAD_SHORTCUT + } + emit changed(false); +} + +void KWinTabBoxConfig::loadConfig(const KConfigGroup& config, KWin::TabBox::TabBoxConfig& tabBoxConfig) +{ + tabBoxConfig.setClientDesktopMode(TabBoxConfig::ClientDesktopMode( + config.readEntry("DesktopMode", TabBoxConfig::defaultDesktopMode()))); + tabBoxConfig.setClientActivitiesMode(TabBoxConfig::ClientActivitiesMode( + config.readEntry("ActivitiesMode", TabBoxConfig::defaultActivitiesMode()))); + tabBoxConfig.setClientApplicationsMode(TabBoxConfig::ClientApplicationsMode( + config.readEntry("ApplicationsMode", TabBoxConfig::defaultApplicationsMode()))); + tabBoxConfig.setClientMinimizedMode(TabBoxConfig::ClientMinimizedMode( + config.readEntry("MinimizedMode", TabBoxConfig::defaultMinimizedMode()))); + tabBoxConfig.setShowDesktopMode(TabBoxConfig::ShowDesktopMode( + config.readEntry("ShowDesktopMode", TabBoxConfig::defaultShowDesktopMode()))); + tabBoxConfig.setClientMultiScreenMode(TabBoxConfig::ClientMultiScreenMode( + config.readEntry("MultiScreenMode", TabBoxConfig::defaultMultiScreenMode()))); + tabBoxConfig.setClientSwitchingMode(TabBoxConfig::ClientSwitchingMode( + config.readEntry("SwitchingMode", TabBoxConfig::defaultSwitchingMode()))); + + tabBoxConfig.setShowTabBox(config.readEntry("ShowTabBox", TabBoxConfig::defaultShowTabBox())); + tabBoxConfig.setHighlightWindows(config.readEntry("HighlightWindows", TabBoxConfig::defaultHighlightWindow())); + + tabBoxConfig.setLayoutName(config.readEntry("LayoutName", TabBoxConfig::defaultLayoutName())); +} + +void KWinTabBoxConfig::saveConfig(KConfigGroup& config, const KWin::TabBox::TabBoxConfig& tabBoxConfig) +{ + // combo boxes + config.writeEntry("DesktopMode", int(tabBoxConfig.clientDesktopMode())); + config.writeEntry("ActivitiesMode", int(tabBoxConfig.clientActivitiesMode())); + config.writeEntry("ApplicationsMode", int(tabBoxConfig.clientApplicationsMode())); + config.writeEntry("MinimizedMode", int(tabBoxConfig.clientMinimizedMode())); + config.writeEntry("ShowDesktopMode", int(tabBoxConfig.showDesktopMode())); + config.writeEntry("MultiScreenMode", int(tabBoxConfig.clientMultiScreenMode())); + config.writeEntry("SwitchingMode", int(tabBoxConfig.clientSwitchingMode())); + config.writeEntry("LayoutName", tabBoxConfig.layoutName()); + + // check boxes + config.writeEntry("ShowTabBox", tabBoxConfig.isShowTabBox()); + config.writeEntry("HighlightWindows", tabBoxConfig.isHighlightWindows()); + + config.sync(); +} + +void KWinTabBoxConfig::save() +{ + KCModule::save(); + KConfigGroup config(m_config, "TabBox"); + + // sync ui to config + updateConfigFromUi(m_primaryTabBoxUi, m_tabBoxConfig); + updateConfigFromUi(m_alternativeTabBoxUi, m_tabBoxAlternativeConfig); + saveConfig(config, m_tabBoxConfig); + config = KConfigGroup(m_config, "TabBoxAlternative"); + saveConfig(config, m_tabBoxAlternativeConfig); + + // effects + bool highlightWindows = m_primaryTabBoxUi->highlightWindowCheck->isChecked() || + m_alternativeTabBoxUi->highlightWindowCheck->isChecked(); + const bool coverSwitch = m_primaryTabBoxUi->showTabBox->isChecked() && + m_primaryTabBoxUi->effectCombo->currentIndex() == CoverSwitch; + const bool flipSwitch = m_primaryTabBoxUi->showTabBox->isChecked() && + m_primaryTabBoxUi->effectCombo->currentIndex() == FlipSwitch; + const bool coverSwitchAlternative = m_alternativeTabBoxUi->showTabBox->isChecked() && + m_alternativeTabBoxUi->effectCombo->currentIndex() == CoverSwitch; + const bool flipSwitchAlternative = m_alternativeTabBoxUi->showTabBox->isChecked() && + m_alternativeTabBoxUi->effectCombo->currentIndex() == FlipSwitch; + + // activate effects if not active + KConfigGroup effectconfig(m_config, "Plugins"); + if (coverSwitch || coverSwitchAlternative) + effectconfig.writeEntry("coverswitchEnabled", true); + if (flipSwitch || flipSwitchAlternative) + effectconfig.writeEntry("flipswitchEnabled", true); + if (highlightWindows) + effectconfig.writeEntry("highlightwindowEnabled", true); + effectconfig.sync(); + KConfigGroup coverswitchconfig(m_config, "Effect-CoverSwitch"); + coverswitchconfig.writeEntry("TabBox", coverSwitch); + coverswitchconfig.writeEntry("TabBoxAlternative", coverSwitchAlternative); + coverswitchconfig.sync(); + KConfigGroup flipswitchconfig(m_config, "Effect-FlipSwitch"); + flipswitchconfig.writeEntry("TabBox", flipSwitch); + flipswitchconfig.writeEntry("TabBoxAlternative", flipSwitchAlternative); + flipswitchconfig.sync(); + + // 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)); + + emit changed(false); +} + +void KWinTabBoxConfig::defaults() +{ + const KWinTabBoxConfigForm* ui[2] = { m_primaryTabBoxUi, m_alternativeTabBoxUi}; + for (int i = 0; i < 2; ++i) { + // combo boxes +#define CONFIGURE(SETTING, MODE, IS, VALUE) \ + ui[i]->SETTING->setChecked(TabBoxConfig::default##MODE##Mode() IS TabBoxConfig::VALUE) + CONFIGURE(filterDesktops, Desktop, !=, AllDesktopsClients); + CONFIGURE(currentDesktop, Desktop, ==, OnlyCurrentDesktopClients); + CONFIGURE(otherDesktops, Desktop, ==, ExcludeCurrentDesktopClients); + CONFIGURE(filterActivities, Activities, !=, AllActivitiesClients); + CONFIGURE(currentActivity, Activities, ==, OnlyCurrentActivityClients); + CONFIGURE(otherActivities, Activities, ==, ExcludeCurrentActivityClients); + CONFIGURE(filterScreens, MultiScreen, !=, IgnoreMultiScreen); + CONFIGURE(currentScreen, MultiScreen, ==, OnlyCurrentScreenClients); + CONFIGURE(otherScreens, MultiScreen, ==, ExcludeCurrentScreenClients); + CONFIGURE(oneAppWindow, Applications, ==, OneWindowPerApplication); + CONFIGURE(filterMinimization, Minimized, !=, IgnoreMinimizedStatus); + CONFIGURE(visibleWindows, Minimized, ==, ExcludeMinimizedClients); + CONFIGURE(hiddenWindows, Minimized, ==, OnlyMinimizedClients); + + ui[i]->switchingModeCombo->setCurrentIndex(TabBoxConfig::defaultSwitchingMode()); + + // checkboxes + ui[i]->showTabBox->setChecked(TabBoxConfig::defaultShowTabBox()); + ui[i]->highlightWindowCheck->setChecked(TabBoxConfig::defaultHighlightWindow()); + CONFIGURE(showDesktop, ShowDesktop, ==, ShowDesktopClient); +#undef CONFIGURE + // effects + ui[i]->effectCombo->setCurrentIndex(ui[i]->effectCombo->findData("sidebar")); + } + + QString action; + auto RESET_SHORTCUT = [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); + }; + RESET_SHORTCUT(m_primaryTabBoxUi->scAll, Qt::ALT + Qt::Key_Tab); + RESET_SHORTCUT(m_primaryTabBoxUi->scAllReverse, Qt::ALT + Qt::SHIFT + Qt::Key_Backtab); + RESET_SHORTCUT(m_alternativeTabBoxUi->scAll); + RESET_SHORTCUT(m_alternativeTabBoxUi->scAllReverse); + RESET_SHORTCUT(m_primaryTabBoxUi->scCurrent, Qt::ALT + Qt::Key_QuoteLeft); + RESET_SHORTCUT(m_primaryTabBoxUi->scCurrentReverse, Qt::ALT + Qt::Key_AsciiTilde); + RESET_SHORTCUT(m_alternativeTabBoxUi->scCurrent); + RESET_SHORTCUT(m_alternativeTabBoxUi->scCurrentReverse); + m_actionCollection->writeSettings(); + emit changed(true); +} + +bool KWinTabBoxConfig::effectEnabled(const BuiltInEffect& effect, const KConfigGroup& cfg) const +{ + return cfg.readEntry(BuiltInEffects::nameForEffect(effect) + "Enabled", BuiltInEffects::enabledByDefault(effect)); +} + +void KWinTabBoxConfig::updateUiFromConfig(KWinTabBoxConfigForm* ui, const KWin::TabBox::TabBoxConfig& config) +{ +#define CONFIGURE(SETTING, MODE, IS, VALUE) ui->SETTING->setChecked(config.MODE##Mode() IS TabBoxConfig::VALUE) + CONFIGURE(filterDesktops, clientDesktop, !=, AllDesktopsClients); + CONFIGURE(currentDesktop, clientDesktop, ==, OnlyCurrentDesktopClients); + CONFIGURE(otherDesktops, clientDesktop, ==, ExcludeCurrentDesktopClients); + CONFIGURE(filterActivities, clientActivities, !=, AllActivitiesClients); + CONFIGURE(currentActivity, clientActivities, ==, OnlyCurrentActivityClients); + CONFIGURE(otherActivities, clientActivities, ==, ExcludeCurrentActivityClients); + CONFIGURE(filterScreens, clientMultiScreen, !=, IgnoreMultiScreen); + CONFIGURE(currentScreen, clientMultiScreen, ==, OnlyCurrentScreenClients); + CONFIGURE(otherScreens, clientMultiScreen, ==, ExcludeCurrentScreenClients); + CONFIGURE(oneAppWindow, clientApplications, ==, OneWindowPerApplication); + CONFIGURE(filterMinimization, clientMinimized, !=, IgnoreMinimizedStatus); + CONFIGURE(visibleWindows, clientMinimized, ==, ExcludeMinimizedClients); + CONFIGURE(hiddenWindows, clientMinimized, ==, OnlyMinimizedClients); + + ui->switchingModeCombo->setCurrentIndex(config.clientSwitchingMode()); + + // check boxes + ui->showTabBox->setChecked(config.isShowTabBox()); + ui->highlightWindowCheck->setChecked(config.isHighlightWindows()); + ui->effectCombo->setCurrentIndex(ui->effectCombo->findData(config.layoutName())); + CONFIGURE(showDesktop, showDesktop, ==, ShowDesktopClient); +#undef CONFIGURE +} + +void KWinTabBoxConfig::updateConfigFromUi(const KWin::KWinTabBoxConfigForm* ui, TabBox::TabBoxConfig& config) +{ + if (ui->filterDesktops->isChecked()) + config.setClientDesktopMode(ui->currentDesktop->isChecked() ? TabBoxConfig::OnlyCurrentDesktopClients : TabBoxConfig::ExcludeCurrentDesktopClients); + else + config.setClientDesktopMode(TabBoxConfig::AllDesktopsClients); + if (ui->filterActivities->isChecked()) + config.setClientActivitiesMode(ui->currentActivity->isChecked() ? TabBoxConfig::OnlyCurrentActivityClients : TabBoxConfig::ExcludeCurrentActivityClients); + else + config.setClientActivitiesMode(TabBoxConfig::AllActivitiesClients); + if (ui->filterScreens->isChecked()) + config.setClientMultiScreenMode(ui->currentScreen->isChecked() ? TabBoxConfig::OnlyCurrentScreenClients : TabBoxConfig::ExcludeCurrentScreenClients); + else + config.setClientMultiScreenMode(TabBoxConfig::IgnoreMultiScreen); + config.setClientApplicationsMode(ui->oneAppWindow->isChecked() ? TabBoxConfig::OneWindowPerApplication : TabBoxConfig::AllWindowsAllApplications); + if (ui->filterMinimization->isChecked()) + config.setClientMinimizedMode(ui->visibleWindows->isChecked() ? TabBoxConfig::ExcludeMinimizedClients : TabBoxConfig::OnlyMinimizedClients); + else + config.setClientMinimizedMode(TabBoxConfig::IgnoreMinimizedStatus); + + config.setClientSwitchingMode(TabBoxConfig::ClientSwitchingMode(ui->switchingModeCombo->currentIndex())); + + config.setShowTabBox(ui->showTabBox->isChecked()); + config.setHighlightWindows(ui->highlightWindowCheck->isChecked()); + if (ui->effectCombo->currentIndex() >= Layout) { + config.setLayoutName(ui->effectCombo->itemData(ui->effectCombo->currentIndex()).toString()); + } + config.setShowDesktopMode(ui->showDesktop->isChecked() ? TabBoxConfig::ShowDesktopClient : TabBoxConfig::DoNotShowDesktopClient); +} + +#define CHECK_CURRENT_TABBOX_UI \ + Q_ASSERT(sender());\ + KWinTabBoxConfigForm *ui = nullptr;\ + QObject *dad = sender();\ + while (!ui && (dad = dad->parent()))\ + ui = qobject_cast(dad);\ + Q_ASSERT(ui); + +void KWinTabBoxConfig::effectSelectionChanged(int index) +{ + CHECK_CURRENT_TABBOX_UI + ui->effectConfigButton->setIcon(QIcon::fromTheme(index < Layout ? "configure" : "view-preview")); + if (!ui->showTabBox->isChecked()) + return; + ui->highlightWindowCheck->setEnabled(index >= Layout); +} + +void KWinTabBoxConfig::tabBoxToggled(bool on) { + CHECK_CURRENT_TABBOX_UI + on = !on || ui->effectCombo->currentIndex() >= Layout; + ui->highlightWindowCheck->setEnabled(on); + emit changed(); +} + +void KWinTabBoxConfig::configureEffectClicked() +{ + CHECK_CURRENT_TABBOX_UI + + const int effect = ui->effectCombo->currentIndex(); + if (effect >= Layout) { + // TODO: here we need to show the preview + new LayoutPreview(ui->effectCombo->itemData(effect, Qt::UserRole+1).toString(), this); + } else { + QPointer configDialog = new QDialog(this); + configDialog->setLayout(new QVBoxLayout); + configDialog->setWindowTitle(ui->effectCombo->currentText()); + 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 = BuiltInEffects::nameForEffect(effect == CoverSwitch ? BuiltInEffect::CoverSwitch : BuiltInEffect::FlipSwitch); + + 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::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(); +} + +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..d454e26 --- /dev/null +++ b/kcmkwin/kwintabbox/main.h @@ -0,0 +1,96 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef __MAIN_H__ +#define __MAIN_H__ + +#include +#include +#include "tabboxconfig.h" + +#include "ui_main.h" + +class KShortcutsEditor; +class KActionCollection; + +namespace KWin +{ +enum class BuiltInEffect; +namespace TabBox +{ + +} + + + +class KWinTabBoxConfigForm : public QWidget, public Ui::KWinTabBoxConfigForm +{ + Q_OBJECT + +public: + explicit KWinTabBoxConfigForm(QWidget* parent); +}; + +class KWinTabBoxConfig : public KCModule +{ + Q_OBJECT + +public: + explicit KWinTabBoxConfig(QWidget* parent, const QVariantList& args); + ~KWinTabBoxConfig(); + +public Q_SLOTS: + virtual void save(); + virtual void load(); + virtual void defaults(); + +private Q_SLOTS: + void effectSelectionChanged(int index); + void configureEffectClicked(); + void tabBoxToggled(bool on); + void shortcutChanged(const QKeySequence &seq); + void slotGHNS(); +private: + void updateUiFromConfig(KWinTabBoxConfigForm* ui, const TabBox::TabBoxConfig& config); + void updateConfigFromUi(const KWinTabBoxConfigForm* ui, TabBox::TabBoxConfig& config); + void loadConfig(const KConfigGroup& config, KWin::TabBox::TabBoxConfig& tabBoxConfig); + void saveConfig(KConfigGroup& config, const KWin::TabBox::TabBoxConfig& tabBoxConfig); + void initLayoutLists(); + +private: + enum Mode { + CoverSwitch = 0, + FlipSwitch = 1, + Layout = 2 + }; + KWinTabBoxConfigForm* m_primaryTabBoxUi; + KWinTabBoxConfigForm* m_alternativeTabBoxUi; + KSharedConfigPtr m_config; + KActionCollection* m_actionCollection; + KShortcutsEditor* m_editor; + TabBox::TabBoxConfig m_tabBoxConfig; + TabBox::TabBoxConfig m_tabBoxAlternativeConfig; + + bool effectEnabled(const BuiltInEffect& effect, const KConfigGroup& cfg) const; +}; + +} // namespace + +#endif diff --git a/kcmkwin/kwintabbox/main.ui b/kcmkwin/kwintabbox/main.ui new file mode 100644 index 0000000..9baa26d --- /dev/null +++ b/kcmkwin/kwintabbox/main.ui @@ -0,0 +1,720 @@ + + + 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::Horizontal + + + + 40 + 20 + + + + + + + + Get New Window Switcher Layout + + + + + + + + + + Qt::Vertical + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + KKeySequenceWidget + QWidget +
kkeysequencewidget.h
+
+ + KComboBox + QComboBox +
kcombobox.h
+
+
+ + highlightWindowCheck + showTabBox + effectCombo + effectConfigButton + ghns + 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 + + + + + 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..8c8edb4 --- /dev/null +++ b/kcmkwin/kwintabbox/thumbnailitem.cpp @@ -0,0 +1,219 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011, 2014 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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", 0 }; + 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..070fe399 --- /dev/null +++ b/kcmkwin/kwintabbox/thumbnailitem.h @@ -0,0 +1,108 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011, 2014 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef KWIN_THUMBNAILITEM_H +#define KWIN_THUMBNAILITEM_H + +#include +#include +#include + +namespace KWin +{ + +class BrightnessSaturationShader : public QSGMaterialShader +{ +public: + BrightnessSaturationShader(); + virtual const char* vertexShader() const override; + virtual const char* fragmentShader() const override; + virtual const char*const* attributeNames() const override; + virtual void updateState(const RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) override; + virtual 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: + virtual 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); + virtual ~WindowThumbnailItem(); + + 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); + virtual 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..23b56a93598a7668a31d29ef8cb11b23fdab91ef GIT binary patch literal 53721 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
`D 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

+@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..60c6f82 --- /dev/null +++ b/libkwineffects/anidata.cpp @@ -0,0 +1,201 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Thomas Lübking + +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, see . +*********************************************************************/ + +#include "anidata_p.h" + +#include "logging_p.h" + +QDebug operator<<(QDebug dbg, const KWin::AniData &a) +{ + dbg.nospace() << a.debugInfo(); + return dbg.space(); +} + +using namespace KWin; +static const int Gaussian = 46; + +AniData::AniData() + : attribute(AnimationEffect::Opacity) + , customCurve(0) // Linear + , time(0) + , duration(0) + , meta(0) + , startTime(0) + , windowType((NET::WindowTypeMask)0) + , waitAtSource(false) + , keepAtTarget(false) +{ +} + +AniData::AniData(AnimationEffect::Attribute a, int meta_, int ms, const FPx2 &to_, + QEasingCurve curve_, int delay, const FPx2 &from_, bool waitAtSource_, bool keepAtTarget_ ) + : attribute(a) + , curve(curve_) + , from(from_) + , to(to_) + , time(0) + , duration(ms) + , meta(meta_) + , startTime(AnimationEffect::clock() + delay) + , windowType((NET::WindowTypeMask)0) + , waitAtSource(waitAtSource_) + , keepAtTarget(keepAtTarget_) +{ +} + +static FPx2 fpx2(const QString &s, AnimationEffect::Attribute a) +{ + bool ok; float f1, f2; + const QVector floats = s.splitRef(u','); + f1 = floats.at(0).toFloat(&ok); + if (!ok || (f1 < 0.0 && !( a == AnimationEffect::Position || + a == AnimationEffect::Translation || + a == AnimationEffect::Size || + a == AnimationEffect::Rotation)) ) { + if (ok) + qCDebug(LIBKWINEFFECTS) << "Invalid value (must not be negative)" << s; + return FPx2(); + } + + bool forced_align = (floats.count() < 2); + if (forced_align) + f2 = f1; + else { + f2 = floats.at(1).toFloat(&ok); + if ( (forced_align = !ok || (f2 < 0.0 && !( a == AnimationEffect::Position || + a == AnimationEffect::Translation || + a == AnimationEffect::Size || + a == AnimationEffect::Rotation))) ) + f2 = f1; + } + if ( forced_align && a >= AnimationEffect::NonFloatBase ) + qCDebug(LIBKWINEFFECTS) << "Generic Animations, WARNING: had to align second dimension of non-onedimensional attribute" << a; + return FPx2(f1, f2); +} + +AniData::AniData(const QString &str) // format: WindowMask:Attribute:Meta:Duration:To:Shape:Delay:From + : customCurve(0) // Linear + , time(0) + , duration(1) // invalidate +{ + const QVector animation = str.splitRef(u':'); + if (animation.count() < 5) + return; // at least window type, attribute, metadata, time and target is required + + windowType = (NET::WindowTypeMask)animation.at(0).toUInt(); + + if (animation.at(1) == QLatin1String("Opacity")) attribute = AnimationEffect::Opacity; + else if (animation.at(1) == QLatin1String("Brightness")) attribute = AnimationEffect::Brightness; + else if (animation.at(1) == QLatin1String("Saturation")) attribute = AnimationEffect::Saturation; + else if (animation.at(1) == QLatin1String("Scale")) attribute = AnimationEffect::Scale; + else if (animation.at(1) == QLatin1String("Translation")) attribute = AnimationEffect::Translation; + else if (animation.at(1) == QLatin1String("Rotation")) attribute = AnimationEffect::Rotation; + else if (animation.at(1) == QLatin1String("Position")) attribute = AnimationEffect::Position; + else if (animation.at(1) == QLatin1String("Size")) attribute = AnimationEffect::Size; + else if (animation.at(1) == QLatin1String("Clip")) attribute = AnimationEffect::Clip; + else { + qCDebug(LIBKWINEFFECTS) << "Invalid attribute" << animation.at(1); + return; + } + + meta = animation.at(2).toUInt(); + + bool ok; + duration = animation.at(3).toInt(&ok); + if (!ok || duration < 0) { + qCDebug(LIBKWINEFFECTS) << "Invalid duration" << animation.at(3); + duration = 0; + return; + } + + to = fpx2(animation.at(4).toString(), attribute); + + if (animation.count() > 5) { + customCurve = animation.at(5).toInt(); + if (customCurve < QEasingCurve::Custom) + curve.setType((QEasingCurve::Type)customCurve); + else if (customCurve == Gaussian) + curve.setCustomType(AnimationEffect::qecGaussian); + else + qCDebug(LIBKWINEFFECTS) << "Unknown curve type" << customCurve; // remains default, ie. linear + + if (animation.count() > 6) { + int t = animation.at(6).toInt(); + if (t < 0) + qCDebug(LIBKWINEFFECTS) << "Delay can not be negative" << animation.at(6); + else + time = t; + + if (animation.count() > 7) + from = fpx2(animation.at(7).toString(), attribute); + } + } + if (!(from.isValid() || to.isValid())) { + duration = -1; // invalidate + return; + } +} + +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(" "); + } +} + +QList AniData::list(const QString &str) +{ + QList newList; + foreach (const QString &astr, str.split(u';', QString::SkipEmptyParts)) { + newList << AniData(astr); + if (newList.last().duration < 0) + newList.removeLast(); + } + return newList; +} + +QString AniData::toString() const +{ + QString ret = QString::number((uint)windowType) + QLatin1Char(':') + attributeString(attribute) + QLatin1Char(':') + + QString::number(meta) + QLatin1Char(':') + QString::number(duration) + QLatin1Char(':') + + to.toString() + QLatin1Char(':') + QString::number(customCurve) + QLatin1Char(':') + + QString::number(time) + QLatin1Char(':') + from.toString(); + return ret; +} + +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(duration) + QLatin1String("ms\n") + + QLatin1String( " Passed: ") + QString::number(time) + QLatin1String("ms\n") + + QLatin1String( " Applying: ") + QString::number(windowType) + QLatin1Char('\n'); +} diff --git a/libkwineffects/anidata_p.h b/libkwineffects/anidata_p.h new file mode 100644 index 0000000..ca9073b --- /dev/null +++ b/libkwineffects/anidata_p.h @@ -0,0 +1,60 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Thomas Lübking + +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, see . +*********************************************************************/ + +#ifndef ANIDATA_H +#define ANIDATA_H + +#include "kwinanimationeffect.h" +#include +#include + +namespace KWin { + +class KWINEFFECTS_EXPORT AniData { +public: + AniData(); + AniData(AnimationEffect::Attribute a, int meta, int ms, const FPx2 &to, + QEasingCurve curve, int delay, const FPx2 &from, bool waitAtSource, bool keepAtTarget = false); + explicit AniData(const QString &str); + inline void addTime(int t) { time += t; } + inline bool isOneDimensional() const { + return from[0] == from[1] && to[0] == to[1]; + } + + quint64 id{0}; + static QList list(const QString &str); + QString toString() const; + QString debugInfo() const; + AnimationEffect::Attribute attribute; + QEasingCurve curve; + int customCurve; + FPx2 from, to; + int time, duration; + uint meta; + qint64 startTime; + NET::WindowTypeMask windowType; + bool waitAtSource, keepAtTarget; +}; + +} // 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..092eb73 --- /dev/null +++ b/libkwineffects/kwinanimationeffect.cpp @@ -0,0 +1,958 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Thomas Lübking + +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, see . +*********************************************************************/ + +#include "kwinanimationeffect.h" +#include "anidata_p.h" + +#include +#include +#include +#include + +QDebug operator<<(QDebug dbg, const KWin::FPx2 &fpx2) +{ + dbg.nospace() << fpx2[0] << "," << fpx2[1] << QString(fpx2.isValid() ? QStringLiteral(" (valid)") : QStringLiteral(" (invalid)")); + return dbg.space(); +} + +namespace KWin { + +QElapsedTimer AnimationEffect::s_clock; + +class AnimationEffectPrivate { +public: + AnimationEffectPrivate() + { + m_animated = m_damageDirty = m_animationsTouched = m_isInitialized = false; + m_justEndedAnimation = 0; + } + AnimationEffect::AniMap m_animations; + EffectWindowList m_zombies; + bool m_animated, m_damageDirty, m_needSceneRepaint, m_animationsTouched, m_isInitialized; + quint64 m_justEndedAnimation; // protect against cancel + static quint64 m_animCounter; +}; +} + +using namespace KWin; + +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, SIGNAL(windowClosed(KWin::EffectWindow*)), SLOT(_windowClosed(KWin::EffectWindow*)) ); + connect ( effects, SIGNAL(windowDeleted(KWin::EffectWindow*)), SLOT(_windowDeleted(KWin::EffectWindow*)) ); +} + +bool AnimationEffect::isActive() const +{ + Q_D(const AnimationEffect); + return !d->m_animations.isEmpty(); +} + + +#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, QEasingCurve curve, int delay, FPx2 from, bool keepAtTarget ) +{ + const bool waitAtSource = from.isValid(); + validate(a, meta, &from, &to, w); + if (a == CrossFadePrevious) + w->referencePreviousWindowPixmap(); + + 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, SIGNAL(windowGeometryShapeChanged(KWin::EffectWindow*,QRect)), + SLOT(_expandedGeometryChanged(KWin::EffectWindow*,QRect))); + connect (effects, SIGNAL(windowStepUserMovedResized(KWin::EffectWindow*,QRect)), + SLOT(_expandedGeometryChanged(KWin::EffectWindow*,QRect))); + connect (effects, SIGNAL(windowPaddingChanged(KWin::EffectWindow*,QRect)), + SLOT(_expandedGeometryChanged(KWin::EffectWindow*,QRect))); + } + AniMap::iterator it = d->m_animations.find(w); + if (it == d->m_animations.end()) + it = d->m_animations.insert(w, QPair, QRect>(QList(), QRect())); + it->first.append(AniData(a, meta, ms, to, curve, delay, from, waitAtSource, keepAtTarget)); + quint64 ret_id = ++d->m_animCounter; + it->first.last().id = ret_id; + it->second = QRect(); + + d->m_animationsTouched = true; + + if (delay > 0) { + QTimer::singleShot(delay, this, SLOT(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->duration = anim->time + newRemainingTime; + return true; + } + } + } + return false; // no animation found +} + +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. + const int i = d->m_zombies.indexOf(entry.key()); + if ( i > -1 ) { + d->m_zombies.removeAt( i ); + entry.key()->unrefWindow(); + } + 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->addTime(time); + } + + if (anim->time < anim->duration || anim->keepAtTarget) { +// if (anim->attribute != Brightness && anim->attribute != Saturation && anim->attribute != Opacity) +// transformed = true; + d->m_animated = true; + ++anim; + ++animCounter; + } else { + EffectWindow *oldW = entry.key(); + AniData *aData = &(*anim); + if (aData->attribute == KWin::AnimationEffect::CrossFadePrevious) { + oldW->unreferencePreviousWindowPixmap(); + effects->addRepaint(oldW->expandedGeometry()); + } + 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()) { + const int i = d->m_zombies.indexOf(entry.key()); + if ( i > -1 ) { + d->m_zombies.removeAt( i ); + entry.key()->unrefWindow(); + } + 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(); + if (!d->m_zombies.isEmpty()) { // this is actually not supposed to happen + foreach (EffectWindow *w, d->m_zombies) + w->unrefWindow(); + d->m_zombies.clear(); + } + } + + 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,SIGNAL(windowGeometryShapeChanged(KWin::EffectWindow*,QRect)), + this, SLOT(_expandedGeometryChanged(KWin::EffectWindow*,QRect))); + disconnect (effects,SIGNAL(windowStepUserMovedResized(KWin::EffectWindow*,QRect)), + this, SLOT(_expandedGeometryChanged(KWin::EffectWindow*,QRect))); + disconnect (effects,SIGNAL(windowPaddingChanged(KWin::EffectWindow*,QRect)), + this, SLOT(_expandedGeometryChanged(KWin::EffectWindow*,QRect))); +} + + +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; + 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(); + data.mask |= PAINT_WINDOW_TRANSFORMED; + if (anim->attribute == Clip) + clipWindow(w, *anim, data.quads); + } + } + if ( isUsed ) { + if ( w->isMinimized() ) + w->enablePainting( EffectWindow::PAINT_DISABLED_BY_MINIMIZE ); + else if ( w->isDeleted() ) + 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->time < anim->duration) { + 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.time < a.duration) + return a.from[i] + a.curve.valueForProgress( ((float)a.time)/a.duration )*(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 +{ + if (a.startTime > clock()) + return 0.0; + if (a.time < a.duration) + return a.curve.valueForProgress( ((float)a.time)/a.duration ); + return 1.0; // we're done and "waiting" at the target value +} + + +// 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.curve.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); + if (d->m_animations.contains(w) && !d->m_zombies.contains(w)) { + w->refWindow(); + d->m_zombies << w; + } +} + +void AnimationEffect::_windowDeleted( EffectWindow* w ) +{ + Q_D(AnimationEffect); + d->m_zombies.removeAll( w ); // TODO this line is a workaround for a bug in KWin 4.8.0 & 4.8.1 + 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; +} + + +#include "moc_kwinanimationeffect.cpp" diff --git a/libkwineffects/kwinanimationeffect.h b/libkwineffects/kwinanimationeffect.h new file mode 100644 index 0000000..deef3c2 --- /dev/null +++ b/libkwineffects/kwinanimationeffect.h @@ -0,0 +1,239 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Thomas Lübking + +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, see . +*********************************************************************/ + +#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[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; +class KWINEFFECTS_EXPORT AnimationEffect : public Effect +{ + Q_OBJECT + Q_ENUMS(Anchor) + Q_ENUMS(Attribute) + Q_ENUMS(MetaType) +public: + typedef QMap< EffectWindow*, QPair, QRect> > AniMap; + + enum Anchor { Left = 1<<0, Top = 1<<1, Right = 1<<2, Bottom = 1<<3, + Horizontal = Left|Right, Vertical = Top|Bottom, Mouse = 1<<4 }; + enum Attribute { + Opacity = 0, Brightness, Saturation, Scale, Rotation, + Position, Size, Translation, Clip, Generic, CrossFadePrevious, + NonFloatBase = Position + }; + enum MetaType { SourceAnchor, TargetAnchor, + RelativeSourceX, RelativeSourceY, RelativeTargetX, RelativeTargetY, Axis }; + /** + * Whenever you intend to connect to the EffectsHandler::windowClosed() signal, do so when reimplementing the constructor. + * Do *not* add private slots named _windowClosed( EffectWindow* w ) or _windowDeleted( EffectWindow* w ) !! + * 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(); + + bool isActive() const; + /** + * Set and get predefined metatypes. + * The first 24 bits are reserved for the AnimationEffect class - you can use the last 8 bits for custom hints. + * In case 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. + */ + static int metaData(MetaType type, uint meta ); + static void setMetaData(MetaType type, uint value, uint &meta ); + + /** + * Reimplemented from KWIn::Effect + */ + QString debug(const QString ¶meter) const; + virtual void prePaintScreen( ScreenPrePaintData& data, int time ); + virtual void prePaintWindow( EffectWindow* w, WindowPrePaintData& data, int time ); + virtual void paintWindow( EffectWindow* w, int mask, QRegion region, WindowPaintData& data ); + virtual void postPaintScreen(); + + /** + * Gaussian (bumper) animation curve for QEasingCurve + */ + static qreal qecGaussian(qreal progress) + { + progress = 2*progress - 1; + progress *= -5*progress; + return qExp(progress); + } + + static inline qint64 clock() { + return s_clock.elapsed(); + } + +protected: + /** + * The central function of this class - call it to create an animated transition of any supported attribute + * @param w - The EffectWindow to manipulate + * @param a - The @enum Attribute to manipulate + * @param meta - Basically a wildcard to carry various extra information, eg. the anchor, relativity or rotation axis. You will probably use require 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 shape - How the animation progresses, eg. 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 + * @return an ID that you can use to cancel a running animation + */ + quint64 animate( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve = QEasingCurve(), int delay = 0, FPx2 from = FPx2() ) + { return p_animate(w, a, meta, ms, to, curve, delay, from, false); } + + /** + * Equal to ::animate() with one important difference: + * The target value for the attribute is kept until you ::cancel() this animation + * @return an ID that you need to use to cancel this manipulation + */ + quint64 set( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve = QEasingCurve(), int delay = 0, FPx2 from = FPx2() ) + { return p_animate(w, a, meta, ms, to, curve, delay, from, true); } + + /** + * this allows to alter the target (but not type or curve) of a running animation + * with the ID @param animationId + * @param newTarget alters the "to" parameter of the animation + * If @param newRemainingTime allows to lengthen (or shorten) the remaining time + * of the animation. By default (-1) the remaining time remains unchanged + * + * Please use @function cancel to cancel an animation rather than altering it. + * NOTICE that you can NOT retarget an animation that just has just @function animationEnded ! + * @return whether there was such animation and it could be altered + */ + bool retarget(quint64 animationId, FPx2 newTarget, int newRemainingTime = -1); + + /** + * Called whenever an animation end, passes the transformed @class EffectWindow @enum Attribute and originally supplied @param meta + * You can reimplement it to keep a constant transformation for the window (ie. keep it a this opacity or position) or to start another animation + */ + virtual void animationEnded( EffectWindow *, Attribute, uint meta ) {Q_UNUSED(meta);} + + /** + * Cancel a running animation. @return true if an animation for @p animationId was found (and canceled) + * NOTICE that there is NO animated reset of the original value. You'll have to provide that with a second animation + * NOTICE as well that this will eventually release a Deleted window. + * 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) + */ + bool cancel(quint64 animationId); + /** + * Called if the transformed @enum Attribute is Generic. You should reimplement it if you transform this "Attribute". + * You could use the meta information to eg. support more than one additional animations + */ + virtual void genericAnimation( EffectWindow *w, WindowPaintData &data, float progress, uint meta ) + {Q_UNUSED(w); Q_UNUSED(data); Q_UNUSED(progress); Q_UNUSED(meta);} + + //Internal for unit tests + AniMap state() const; + +private: + quint64 p_animate( EffectWindow *w, Attribute a, uint meta, int ms, FPx2 to, QEasingCurve curve, int delay, FPx2 from, bool keepAtTarget ); + 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) +}; + + +} // namespace +QDebug operator<<(QDebug dbg, const KWin::FPx2 &fpx2); +Q_DECLARE_METATYPE(KWin::FPx2) + +#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/kwineffects.cpp b/libkwineffects/kwineffects.cpp new file mode 100644 index 0000000..437e662 --- /dev/null +++ b/libkwineffects/kwineffects.cpp @@ -0,0 +1,2072 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2009 Lucas Murray +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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 + +#include + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING +#include +#endif + +#if defined(__GNUC__) +# define KWIN_ALIGN(n) __attribute((aligned(n))) +# if defined(__SSE2__) +# define HAVE_SSE2 +# endif +#elif defined(__INTEL_COMPILER) +# define KWIN_ALIGN(n) __declspec(align(n)) +# define HAVE_SSE2 +#else +# define KWIN_ALIGN(n) +#endif + +#ifdef HAVE_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; +}; + +ScreenPaintData::ScreenPaintData() + : PaintData() + , d(new Private()) +{ +} + +ScreenPaintData::ScreenPaintData(const QMatrix4x4 &projectionMatrix, const QRect &outputGeometry) + : PaintData() + , d(new Private()) +{ + d->projectionMatrix = projectionMatrix; + d->outputGeometry = outputGeometry; +} + +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; +} + +//**************************************** +// 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, QRegion region, 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, QRegion region, 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, QRegion region, 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(quint32 id, const QPointF &pos, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(pos) + Q_UNUSED(time) + return false; +} + +bool Effect::touchMotion(quint32 id, const QPointF &pos, quint32 time) +{ + Q_UNUSED(id) + Q_UNUSED(pos) + Q_UNUSED(time) + return false; +} + +bool Effect::touchUp(quint32 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 + 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; + bool managed = false; +}; + +EffectWindow::Private::Private(EffectWindow *q) + : q(q) +{ +} + +EffectWindow::EffectWindow(QObject *parent) + : QObject(parent) + , d(new Private(this)) +{ + // 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, ShellClient, or Unmanaged. So, later on, when + // an instance of Deleted becomes parent of the EffectWindow, effects + // can still figure out whether it is/was a managed window. + d->managed = parent->property("managed").value(); +} + +EffectWindow::~EffectWindow() +{ +} + +#define WINDOW_HELPER( rettype, prototype, propertyname ) \ + rettype EffectWindow::prototype ( ) const \ + { \ + return parent()->property( propertyname ).value< rettype >(); \ + } + +WINDOW_HELPER(double, opacity, "opacity") +WINDOW_HELPER(bool, hasAlpha, "alpha") +WINDOW_HELPER(int, x, "x") +WINDOW_HELPER(int, y, "y") +WINDOW_HELPER(int, width, "width") +WINDOW_HELPER(int, height, "height") +WINDOW_HELPER(QPoint, pos, "pos") +WINDOW_HELPER(QSize, size, "size") +WINDOW_HELPER(int, screen, "screen") +WINDOW_HELPER(QRect, geometry, "geometry") +WINDOW_HELPER(QRect, expandedGeometry, "visibleRect") +WINDOW_HELPER(QRect, rect, "rect") +WINDOW_HELPER(int, desktop, "desktop") +WINDOW_HELPER(bool, isDesktop, "desktopWindow") +WINDOW_HELPER(bool, isDock, "dock") +WINDOW_HELPER(bool, isToolbar, "toolbar") +WINDOW_HELPER(bool, isMenu, "menu") +WINDOW_HELPER(bool, isNormalWindow, "normalWindow") +WINDOW_HELPER(bool, isDialog, "dialog") +WINDOW_HELPER(bool, isSplash, "splash") +WINDOW_HELPER(bool, isUtility, "utility") +WINDOW_HELPER(bool, isDropdownMenu, "dropdownMenu") +WINDOW_HELPER(bool, isPopupMenu, "popupMenu") +WINDOW_HELPER(bool, isTooltip, "tooltip") +WINDOW_HELPER(bool, isNotification, "notification") +WINDOW_HELPER(bool, isOnScreenDisplay, "onScreenDisplay") +WINDOW_HELPER(bool, isComboBox, "comboBox") +WINDOW_HELPER(bool, isDNDIcon, "dndIcon") +WINDOW_HELPER(bool, isDeleted, "deleted") +WINDOW_HELPER(bool, hasOwnShape, "shaped") +WINDOW_HELPER(QString, windowRole, "windowRole") +WINDOW_HELPER(QStringList, activities, "activities") +WINDOW_HELPER(bool, skipsCloseAnimation, "skipsCloseAnimation") +WINDOW_HELPER(KWayland::Server::SurfaceInterface *, surface, "surface") + +QString EffectWindow::windowClass() const +{ + return parent()->property("resourceName").toString() + QLatin1Char(' ') + parent()->property("resourceClass").toString(); +} + +QRect EffectWindow::contentsRect() const +{ + return QRect(parent()->property("clientPos").toPoint(), parent()->property("clientSize").toSize()); +} + +NET::WindowType EffectWindow::windowType() const +{ + return static_cast(parent()->property("windowType").toInt()); +} + +bool EffectWindow::isOnActivity(QString activity) const +{ + const QStringList activities = parent()->property("activities").toStringList(); + return activities.isEmpty() || activities.contains(activity); +} + +bool EffectWindow::isOnAllActivities() const +{ + return parent()->property("activities").toStringList().isEmpty(); +} + +#undef WINDOW_HELPER + +#define WINDOW_HELPER_DEFAULT( rettype, prototype, propertyname, defaultValue ) \ + rettype EffectWindow::prototype ( ) const \ + { \ + const QVariant variant = parent()->property( propertyname ); \ + if (!variant.isValid()) { \ + return defaultValue; \ + } \ + return variant.value< rettype >(); \ + } + +WINDOW_HELPER_DEFAULT(bool, isMinimized, "minimized", false) +WINDOW_HELPER_DEFAULT(bool, isMovable, "moveable", false) +WINDOW_HELPER_DEFAULT(bool, isMovableAcrossScreens, "moveableAcrossScreens", false) +WINDOW_HELPER_DEFAULT(QString, caption, "caption", QString()) +WINDOW_HELPER_DEFAULT(bool, keepAbove, "keepAbove", true) +WINDOW_HELPER_DEFAULT(bool, keepBelow, "keepBelow", false) +WINDOW_HELPER_DEFAULT(bool, isModal, "modal", false) +WINDOW_HELPER_DEFAULT(QSize, basicUnit, "basicUnit", QSize(1, 1)) +WINDOW_HELPER_DEFAULT(bool, isUserMove, "move", false) +WINDOW_HELPER_DEFAULT(bool, isUserResize, "resize", false) +WINDOW_HELPER_DEFAULT(QRect, iconGeometry, "iconGeometry", QRect()) +WINDOW_HELPER_DEFAULT(bool, isSpecialWindow, "specialWindow", true) +WINDOW_HELPER_DEFAULT(bool, acceptsFocus, "wantsInput", true) // We don't actually know... +WINDOW_HELPER_DEFAULT(QIcon, icon, "icon", QIcon()) +WINDOW_HELPER_DEFAULT(bool, isSkipSwitcher, "skipSwitcher", false) +WINDOW_HELPER_DEFAULT(bool, isCurrentTab, "isCurrentTab", true) +WINDOW_HELPER_DEFAULT(bool, decorationHasAlpha, "decorationHasAlpha", false) +WINDOW_HELPER_DEFAULT(bool, isFullScreen, "fullScreen", false) +WINDOW_HELPER_DEFAULT(bool, isUnresponsive, "unresponsive", false) + +#undef WINDOW_HELPER_DEFAULT + +#define WINDOW_HELPER_SETTER( prototype, propertyname, args, value ) \ + void EffectWindow::prototype ( args ) \ + {\ + const QVariant variant = parent()->property( propertyname ); \ + if (variant.isValid()) { \ + parent()->setProperty( propertyname, value ); \ + } \ + } + +WINDOW_HELPER_SETTER(minimize, "minimized",,true) +WINDOW_HELPER_SETTER(unminimize, "minimized",,false) + +#undef WINDOW_HELPER_SETTER + +void EffectWindow::setMinimized(bool min) +{ + if (min) { + minimize(); + } else { + unminimize(); + } +} + +void EffectWindow::closeWindow() const +{ + QMetaObject::invokeMethod(parent(), "closeWindow"); +} + +void EffectWindow::addRepaint(int x, int y, int w, int h) +{ + QMetaObject::invokeMethod(parent(), "addRepaint", Q_ARG(int, x), Q_ARG(int, y), Q_ARG(int, w), Q_ARG(int, h)); +} + +void EffectWindow::addRepaint(const QRect &r) +{ + QMetaObject::invokeMethod(parent(), "addRepaint", Q_ARG(const QRect&, r)); +} + +void EffectWindow::addRepaintFull() +{ + QMetaObject::invokeMethod(parent(), "addRepaintFull"); +} + +void EffectWindow::addLayerRepaint(int x, int y, int w, int h) +{ + QMetaObject::invokeMethod(parent(), "addLayerRepaint", Q_ARG(int, x), Q_ARG(int, y), Q_ARG(int, w), Q_ARG(int, h)); +} + +void EffectWindow::addLayerRepaint(const QRect &r) +{ + QMetaObject::invokeMethod(parent(), "addLayerRepaint", Q_ARG(const QRect&, r)); +} + +bool EffectWindow::isOnCurrentActivity() const +{ + return isOnActivity(effects->currentActivity()); +} + +bool EffectWindow::isOnCurrentDesktop() const +{ + return isOnDesktop(effects->currentDesktop()); +} + +bool EffectWindow::isOnDesktop(int d) const +{ + return desktop() == d || isOnAllDesktops(); +} + +bool EffectWindow::isOnAllDesktops() const +{ + return desktop() == NET::OnAllDesktops; +} + +bool EffectWindow::hasDecoration() const +{ + return contentsRect() != QRect(0, 0, width(), height()); +} + +bool EffectWindow::isVisible() const +{ + return !isMinimized() + && isOnCurrentDesktop() + && isOnCurrentActivity(); +} + +bool EffectWindow::isManaged() const +{ + return d->managed; +} + + +//**************************************** +// EffectWindowGroup +//**************************************** + +EffectWindowGroup::~EffectWindowGroup() +{ +} + +/*************************************************************** + WindowQuad +***************************************************************/ + +WindowQuad WindowQuad::makeSubQuad(double x1, double y1, double x2, double y2) const +{ + assert(x1 < x2 && y1 < y2 && x1 >= left() && x2 <= right() && y1 >= top() && y2 <= bottom()); +#ifndef NDEBUG + 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; + foreach (const WindowQuad & quad, *this) { +#ifndef NDEBUG + 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; + foreach (const WindowQuad & quad, *this) { +#ifndef NDEBUG + 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) { +#ifndef NDEBUG + 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; + + foreach (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(); + + foreach (const WindowQuad &quad, *this) { +#ifndef NDEBUG + 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; + + foreach (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; + + assert(type == GL_QUADS || type == GL_TRIANGLES); + + switch (type) + { + case GL_QUADS: +#ifdef HAVE_SSE2 + if (!(intptr_t(vertex) & 0xf)) { + for (int i = 0; i < count(); i++) { + const WindowQuad &quad = at(i); + KWIN_ALIGN(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 = (const __m128i *) &v; + __m128i *dstP = (__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 // HAVE_SSE2 + { + for (int i = 0; i < count(); i++) { + const WindowQuad &quad = at(i); + + 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: +#ifdef HAVE_SSE2 + if (!(intptr_t(vertex) & 0xf)) { + for (int i = 0; i < count(); i++) { + const WindowQuad &quad = at(i); + KWIN_ALIGN(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 = (const __m128i *) &v; + __m128i *dstP = (__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 // HAVE_SSE2 + { + for (int i = 0; i < count(); i++) { + const WindowQuad &quad = at(i); + 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 (int i = 0; i < count(); i++) { + const WindowQuad &quad = at(i); + + 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 +{ + foreach (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 +{ + foreach (const WindowQuad & q, *this) + if (q.smoothNeeded()) + return true; + return false; +} + +bool WindowQuadList::isTransformed() const +{ + foreach (const WindowQuad & q, *this) + if (q.isTransformed()) + return true; + return false; +} + +/*************************************************************** + 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() +{ + assert(areas != nullptr); // can be called only with clip() == true + const QSize &s = effects->virtualScreenSize(); + QRegion ret = QRegion(0, 0, s.width(), s.height()); + foreach (const QRegion & r, *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; +}; + +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; + } + if (d->elapsed > std::chrono::milliseconds::zero()) { + d->elapsed = d->duration - d->elapsed; + } + d->direction = direction; +} + +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 &TimeLine::operator=(const TimeLine &other) +{ + d = other.d; + return *this; +} + +} // namespace + diff --git a/libkwineffects/kwineffects.h b/libkwineffects/kwineffects.h new file mode 100644 index 0000000..a439132 --- /dev/null +++ b/libkwineffects/kwineffects.h @@ -0,0 +1,3722 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2009 Lucas Murray +Copyright (C) 2010, 2011 Martin Gräßlin +Copyright (C) 2018 Vlad Zagorodniy + +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, see . +*********************************************************************/ + +#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 + +#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 KWayland { + namespace Server { + class SurfaceInterface; + class Display; + } +} + +namespace KWin +{ + +class PaintDataPrivate; +class WindowPaintDataPrivate; + +class EffectWindow; +class EffectWindowGroup; +class EffectFrame; +class EffectFramePrivate; +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 226 +#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. + **/ + virtual ~Effect(); + + /** + * 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, QRegion region, 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, QRegion region, 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, QRegion region, 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(quint32 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(quint32 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(quint32 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(); + virtual ~EffectPluginFactory(); + /** + * 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) + friend class Effect; +public: + explicit EffectsHandler(CompositingType type); + virtual ~EffectsHandler(); + // for use by effects + virtual void prePaintScreen(ScreenPrePaintData& data, int time) = 0; + virtual void paintScreen(int mask, QRegion region, ScreenPaintData& data) = 0; + virtual void postPaintScreen() = 0; + virtual void prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) = 0; + virtual void paintWindow(EffectWindow* w, int mask, QRegion region, WindowPaintData& data) = 0; + virtual void postPaintWindow(EffectWindow* w) = 0; + virtual void paintEffectFrame(EffectFrame* frame, QRegion region, double opacity, double frameOpacity) = 0; + virtual void drawWindow(EffectWindow* w, int mask, QRegion region, 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 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 @link{QAction::triggered} + * signal gets invoked. + * + * To unregister the touch screen action either delete the @p action or + * invoke @link{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; + Q_SCRIPTABLE virtual void windowToDesktop(KWin::EffectWindow* w, int desktop) = 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(KWayland::Server::SurfaceInterface *surf) 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 @link removeSupportProperty. When an Effect is + * destroyed it is automatically taken care of removing the support. It is not + * required to call @link 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 KWayland::Server::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 @link{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; + +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 + * @link EffectWindow::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 + * @link EffectWindow::isUserMove or @link EffectWindow::isUserResize. + * Whenever the geometry is updated the signal @link windowStepUserMovedResized + * is emitted with the current geometry. + * The move/resize operation ends with the signal @link 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); + /** + * 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 @link 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 @link 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 @link 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 @link 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 @link startMousePolling. + * For a fullscreen effect it is better to use an input window and react on @link 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 @link 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 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 @link{windowAdded} and hidden with @link{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 @link{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 @link{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(); + +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 http://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 http://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 http://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 http://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 http://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 http://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 http://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 http://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 http://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 http://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 http://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 http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool notification READ isNotification) + /** + * 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 http://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 http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + Q_PROPERTY(bool dndIcon READ isDNDIcon) + /** + * Returns the NETWM window type + * See http://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 http://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: + *

+ * @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(KWayland::Server::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) +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, + /** 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); + virtual ~EffectWindow(); + + virtual void enablePainting(int reason) = 0; + virtual void disablePainting(int reason) = 0; + virtual bool isPaintingEnabled() = 0; + Q_SCRIPTABLE void addRepaint(const QRect& r); + Q_SCRIPTABLE void addRepaint(int x, int y, int w, int h); + Q_SCRIPTABLE void addRepaintFull(); + Q_SCRIPTABLE void addLayerRepaint(const QRect& r); + Q_SCRIPTABLE void addLayerRepaint(int x, int y, int w, int h); + + virtual void refWindow() = 0; + virtual void unrefWindow() = 0; + bool isDeleted() const; + + bool isMinimized() const; + double opacity() const; + bool hasAlpha() const; + + bool isOnCurrentActivity() const; + Q_SCRIPTABLE bool isOnActivity(QString id) const; + bool isOnAllActivities() const; + QStringList activities() const; + + bool isOnDesktop(int d) const; + bool isOnCurrentDesktop() const; + bool isOnAllDesktops() const; + int desktop() const; // prefer isOnXXX() + + int x() const; + int y() const; + int width() const; + int height() const; + /** + * By how much the window wishes to grow/shrink at least. Usually QSize(1,1). + * MAY BE DISOBEYED BY THE WM! It's only for information, do NOT rely on it at all. + */ + QSize basicUnit() const; + QRect geometry() const; + /** + * Geometry of the window including decoration and potentially shadows. + * May be different from geometry() if the window has a shadow. + * @since 4.9 + */ + QRect expandedGeometry() const; + virtual QRegion shape() const = 0; + int screen() const; + /** @internal Do not use */ + bool hasOwnShape() const; // only for shadow effect, for now + QPoint pos() const; + QSize size() const; + QRect rect() const; + bool isMovable() const; + bool isMovableAcrossScreens() const; + bool isUserMove() const; + bool isUserResize() const; + QRect iconGeometry() const; + + /** + * Geometry of the actual window contents inside the whole (including decorations) window. + */ + QRect contentsRect() const; + /** + * 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; + bool decorationHasAlpha() const; + virtual QByteArray readProperty(long atom, long type, int format) const = 0; + virtual void deleteProperty(long atom) const = 0; + + QString caption() const; + QIcon icon() const; + QString windowClass() const; + QString windowRole() const; + 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 http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isDesktop() const; + /** + * Returns whether the window is a dock (i.e. a panel). + * See _NET_WM_WINDOW_TYPE_DOCK at http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isDock() const; + /** + * Returns whether the window is a standalone (detached) toolbar window. + * See _NET_WM_WINDOW_TYPE_TOOLBAR at http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isToolbar() const; + /** + * Returns whether the window is a torn-off menu. + * See _NET_WM_WINDOW_TYPE_MENU at http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isMenu() const; + /** + * Returns whether the window is a "normal" window, i.e. an application or any other window + * for which none of the specialized window types fit. + * See _NET_WM_WINDOW_TYPE_NORMAL at http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isNormalWindow() const; // normal as in 'NET::Normal or NET::Unknown non-transient' + /** + * Returns whether the window is any of special windows types (desktop, dock, splash, ...), + * i.e. window types that usually don't have a window frame and the user does not use window + * management (moving, raising,...) on them. + */ + bool isSpecialWindow() const; + /** + * Returns whether the window is a dialog window. + * See _NET_WM_WINDOW_TYPE_DIALOG at http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isDialog() const; + /** + * Returns whether the window is a splashscreen. Note that many (especially older) applications + * do not support marking their splash windows with this type. + * See _NET_WM_WINDOW_TYPE_SPLASH at http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isSplash() const; + /** + * Returns whether the window is a utility window, such as a tool window. + * See _NET_WM_WINDOW_TYPE_UTILITY at http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isUtility() const; + /** + * 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 http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isDropdownMenu() const; + /** + * Returns whether the window is a popup menu (that is not a torn-off or dropdown menu). + * See _NET_WM_WINDOW_TYPE_POPUP_MENU at http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isPopupMenu() const; // a context popup, not dropdown, not torn-off + /** + * Returns whether the window is a tooltip. + * See _NET_WM_WINDOW_TYPE_TOOLTIP at http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isTooltip() const; + /** + * Returns whether the window is a window with a notification. + * See _NET_WM_WINDOW_TYPE_NOTIFICATION at http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isNotification() const; + /** + * Returns whether the window is an on screen display window + * using the non-standard _KDE_NET_WM_WINDOW_TYPE_ON_SCREEN_DISPLAY + */ + bool isOnScreenDisplay() const; + /** + * Returns whether the window is a combobox popup. + * See _NET_WM_WINDOW_TYPE_COMBO at http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isComboBox() const; + /** + * Returns whether the window is a Drag&Drop icon. + * See _NET_WM_WINDOW_TYPE_DND at http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + bool isDNDIcon() const; + /** + * Returns the NETWM window type + * See http://standards.freedesktop.org/wm-spec/wm-spec-latest.html . + */ + NET::WindowType windowType() const; + /** + * Returns whether the window is managed by KWin (it has control over its placement and other + * aspects, as opposed to override-redirect windows that are entirely handled by the application). + */ + bool isManaged() const; // whether it's managed or override-redirect + /** + * Returns whether or not the window can accept keyboard focus. + */ + bool acceptsFocus() const; + /** + * Returns whether or not the window is kept above all other windows. + */ + bool keepAbove() const; + /** + * Returns whether the window is kept below all other windows. + */ + bool keepBelow() const; + + bool isModal() const; + Q_SCRIPTABLE virtual KWin::EffectWindow* findModal() = 0; + Q_SCRIPTABLE virtual QList mainWindows() const = 0; + + /** + * Returns whether the window should be excluded from window switching effects. + * @since 4.5 + */ + bool isSkipSwitcher() const; + + /** + * 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); + void minimize(); + void unminimize(); + Q_SCRIPTABLE void closeWindow() const; + + bool isCurrentTab() const; + + /** + * @since 4.11 + **/ + bool isVisible() const; + + /** + * @since 5.0 + **/ + bool skipsCloseAnimation() const; + + /** + * @since 5.5 + */ + KWayland::Server::SurfaceInterface *surface() const; + + /** + * @since 5.6 + **/ + bool isFullScreen() const; + + /** + * @since 5.10 + */ + bool isUnresponsive() const; + + /** + * Can be used to by effects to store arbitrary data in the EffectWindow. + * + * Invoking this method will emit the signal EffectsHandler::windowDataChanged. + * @see EffectsHandler::windowDataChanged + */ + Q_SCRIPTABLE 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 @link 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 @link 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 QList< WindowQuad > +{ +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); + virtual ~WindowPaintData(); + /** + * 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()); + ScreenPaintData(const ScreenPaintData &other); + virtual ~ScreenPaintData(); + /** + * 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; +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(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(QRegion region = 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::InQuad, 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(); + + 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) +{ + assert(index >= 0 && index < 4); + return verts[ index ]; +} + +inline +const WindowVertex& WindowQuad::operator[](int index) const +{ + assert(index >= 0 && index < 4); + return verts[ index ]; +} + +inline +WindowQuadType WindowQuad::type() const +{ + assert(quadType != WindowQuadError); + return quadType; +} + +inline +int WindowQuad::id() const +{ + return quadID; +} + +inline +bool WindowQuad::decoration() const +{ + assert(quadType != WindowQuadError); + return quadType == WindowQuadDecoration; +} + +inline +bool WindowQuad::effect() const +{ + 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) + +/** @} */ + +#endif // KWINEFFECTS_H diff --git a/libkwineffects/kwinglobals.h b/libkwineffects/kwinglobals.h new file mode 100644 index 0000000..bcedbfa --- /dev/null +++ b/libkwineffects/kwinglobals.h @@ -0,0 +1,256 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak + +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, see . +*********************************************************************/ + +#ifndef KWIN_LIB_KWINGLOBALS_H +#define KWIN_LIB_KWINGLOBALS_H + +#include +#include +#include +#include + +#include + +#include + +#include + +#define KWIN_QT5_PORTING 0 + +namespace KWin +{ + + +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 +}; + +inline +KWIN_EXPORT xcb_connection_t *connection() +{ + static xcb_connection_t *s_con = nullptr; + if (!s_con) { + s_con = reinterpret_cast(qApp->property("x11Connection").value()); + } + Q_ASSERT(qApp); + return s_con; +} + +inline +KWIN_EXPORT xcb_window_t rootWindow() +{ + static xcb_window_t s_rootWindow = XCB_WINDOW_NONE; + if (s_rootWindow == XCB_WINDOW_NONE) { + s_rootWindow = qApp->property("x11RootWindow").value(); + } + return s_rootWindow; +} + +inline +KWIN_EXPORT xcb_timestamp_t xTime() +{ + return qApp->property("x11Time").value(); +} + +inline +KWIN_EXPORT xcb_screen_t *defaultScreen() +{ + static xcb_screen_t *s_screen = nullptr; + if (s_screen) { + return s_screen; + } + int screen = qApp->property("x11ScreenNumber").toInt(); + for (xcb_screen_iterator_t it = xcb_setup_roots_iterator(xcb_get_setup(connection())); + it.rem; + --screen, xcb_screen_next(&it)) { + if (screen == 0) { + s_screen = it.data; + } + } + return s_screen; +} + +inline +KWIN_DEPRECATED_EXPORT int displayWidth() +{ + xcb_screen_t *screen = defaultScreen(); + return screen ? screen->width_in_pixels : 0; +} + +inline +KWIN_DEPRECATED_EXPORT int displayHeight() +{ + xcb_screen_t *screen = defaultScreen(); + return screen ? screen->height_in_pixels : 0; +} + +/** + * 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; + + 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..cb571da --- /dev/null +++ b/libkwineffects/kwinglplatform.cpp @@ -0,0 +1,1162 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2010 Fredrik Höglund + +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, see . +*********************************************************************/ + +#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 = 0; + +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 &string, const QString &match, int offset = 0) +{ + QString result; + QRegExp rx(match); + int pos = rx.indexIn(string, offset); + if (pos != -1) + result = string.mid(pos, rx.matchedLength()); + return result; +} + +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; + + 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(0, 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_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"); + + 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("NI"); + + 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); + m_extensions = QSet::fromList(extensions.split(' ')); + } + + // 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; + } + + // Nouveau + else if (m_vendor == "nouveau") { + m_chipClass = detectNVidiaClass(m_chipset); + m_driver = Driver_Nouveau; + } + + // softpipe + else if (m_vendor == "VMware, Inc." && m_chipset == "softpipe" ) { + m_driver = Driver_Softpipe; + } + + // llvmpipe + else if (m_vendor == "VMware, Inc." && m_chipset == "llvmpipe") { + m_driver = Driver_Llvmpipe; + } + + // SVGA3D + else if (m_vendor == "VMware, Inc." && m_chipset.contains("SVGA3D")) { + m_driver = Driver_VMware; + } + } + + // 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; + } + + // 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::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..62dc396 --- /dev/null +++ b/libkwineffects/kwinglplatform.h @@ -0,0 +1,433 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2010 Fredrik Höglund + +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, see . +*********************************************************************/ + +#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_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 + 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 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..3818fe2 --- /dev/null +++ b/libkwineffects/kwingltexture.cpp @@ -0,0 +1,658 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006-2007 Rivo Laks +Copyright (C) 2010, 2011 Martin Gräßlin +Copyright (C) 2012 Philipp Knechtges + +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, see . +*********************************************************************/ + +#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) + : 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) { + 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) + : GLTexture(internalFormat, size.width(), size.height(), levels) +{ +} + +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_mipLevels(1) + , m_unnormalizeActive(0) + , m_normalizeActive(0) + , m_vbo(nullptr) +{ + ++s_textureObjectCounter; +} + +GLTexturePrivate::~GLTexturePrivate() +{ + delete m_vbo; + if (m_texture != 0) { + 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); + + 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(QRegion region, 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); + 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 + 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); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + } 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); + 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; +} + +} // namespace KWin diff --git a/libkwineffects/kwingltexture.h b/libkwineffects/kwingltexture.h new file mode 100644 index 0000000..6a212de --- /dev/null +++ b/libkwineffects/kwingltexture.h @@ -0,0 +1,153 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006-2007 Rivo Laks +Copyright (C) 2010, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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); + explicit GLTexture(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(QRegion region, const QRect& rect, bool hardwareClipping = false); + + GLuint texture() const; + GLenum target() const; + GLenum filter() const; + GLenum internalFormat() const; + + /** @short + * Make the texture fully transparent + * Warning: this clobbers the current framebuffer binding except on fglrx + */ + 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..0eaca55 --- /dev/null +++ b/libkwineffects/kwingltexture_p.h @@ -0,0 +1,91 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006-2007 Rivo Laks +Copyright (C) 2010, 2011 Martin Gräßlin +Copyright (C) 2011 Philipp Knechtges + +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, see . +*********************************************************************/ + +#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; + 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..5dbcc54 --- /dev/null +++ b/libkwineffects/kwinglutils.cpp @@ -0,0 +1,2319 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006-2007 Rivo Laks +Copyright (C) 2010, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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(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:" << endl << 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"); + } + if (ShaderManager::instance()->isShaderDebug()) { + ba.append("#define KWIN_SHADER_DEBUG 1\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:" << endl << 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"); + + 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() +{ + m_debug = qstrcmp(qgetenv("KWIN_GL_DEBUG"), "1") == 0; + + 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 (output != QByteArrayLiteral("gl_FragColor")) + stream << "\nout vec4 " << output << ";\n"; + + stream << "\nvoid main(void)\n{\n"; + if (traits & ShaderTrait::MapTexture) { + if (traits & (ShaderTrait::Modulate | ShaderTrait::AdjustSaturation)) { + stream << " vec4 texel = " << textureLookup << "(sampler, texcoord0);\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, texcoord0);\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(); +} + +bool ShaderManager::isShaderDebug() const +{ + return m_debug; +} + +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]; + +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, 0); + 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, 0); + glDeleteFramebuffers(1, &mFramebuffer); + return; + } +#endif + + const GLenum status = glCheckFramebufferStatus(GL_FRAMEBUFFER); + + glBindFramebuffer(GL_FRAMEBUFFER, 0); + + 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, 0); + 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 accomodate(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::accomodate(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(); + } + + 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, 0, 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; + + assert(index >= 0 && index < VertexAttributeCount); + 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->accomodate(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..b004511 --- /dev/null +++ b/libkwineffects/kwinglutils.h @@ -0,0 +1,825 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006-2007 Rivo Laks +Copyright (C) 2010, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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(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, + 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), +}; + +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; + /** + * Is @c true if the environment variable KWIN_GL_DEBUG is set to 1. + * In that case shaders are compiled with KWIN_SHADER_DEBUG defined. + * @returns @c true if shaders are compiled with debug information + * @since 4.8 + **/ + bool isShaderDebug() 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 @link 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 vertesSource 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 @link {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; + bool m_debug; + 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 @link 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; + } + + +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]; + + 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..0ea13d9 --- /dev/null +++ b/libkwineffects/kwinglutils_funcs.cpp @@ -0,0 +1,102 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks + +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, see . +*********************************************************************/ + +#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(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 http://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 http://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..3657751 --- /dev/null +++ b/libkwineffects/kwinglutils_funcs.h @@ -0,0 +1,60 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2007 Rivo Laks + +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, see . +*********************************************************************/ + +#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(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..1c2108b --- /dev/null +++ b/libkwineffects/kwinxrenderutils.cpp @@ -0,0 +1,296 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2008 Lubos Lunak + +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, see . +*********************************************************************/ + +#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.byteCount(), 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< QRect > rects = region.rects(); + QVector< xcb_rectangle_t > xrects(rects.count()); + for (int i = 0; + i < rects.count(); + ++i) { + const QRect &rect = rects.at(i); + xcb_rectangle_t xrect; + xrect.x = rect.x(); + xrect.y = rect.y(); + xrect.width = rect.width(); + xrect.height = rect.height(); + xrects[i] = 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..2413fa1 --- /dev/null +++ b/libkwineffects/kwinxrenderutils.h @@ -0,0 +1,194 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2008 Lubos Lunak + +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, see . +*********************************************************************/ + +#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..6b2c61e --- /dev/null +++ b/libkwineffects/logging.cpp @@ -0,0 +1,23 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..394d3d5 --- /dev/null +++ b/libkwineffects/logging_p.h @@ -0,0 +1,30 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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/logind.cpp b/logind.cpp new file mode 100644 index 0000000..53d5666 --- /dev/null +++ b/logind.cpp @@ -0,0 +1,461 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(); + } + ); +} + +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")})); + 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; + 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..40e3c4f --- /dev/null +++ b/logind.h @@ -0,0 +1,112 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(); + + 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); + +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/main.cpp b/main.cpp new file mode 100644 index 0000000..9f58432 --- /dev/null +++ b/main.cpp @@ -0,0 +1,458 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ + +#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 + +// 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_inputConfig() + , m_operationMode(mode) +{ + qRegisterMetaType("Options::WindowOperation"); + qRegisterMetaType(); + qRegisterMetaType("KWayland::Server::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); + } + if (!m_inputConfig) { + m_inputConfig = KSharedConfig::openConfig(QStringLiteral("kcminputrc"), KConfig::NoGlobals); + } + + performStartup(); +} + +Application::~Application() +{ + delete options; + destroyAtoms(); +} + +void Application::destroyAtoms() +{ + delete atoms; + atoms = 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-2013, 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 Gräßlin"), i18n("Maintainer"), QStringLiteral("mgraesslin@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->addVersionOption(); + parser->addHelpOption(); + parser->addOption(lockOption); + parser->addOption(crashesOption); + KAboutData::applicationData().setupCommandLine(parser); +} + +void Application::processCommandLine(QCommandLineParser *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::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 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(m_originalSessionKey); + 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::createCompositor() +{ + Compositor::create(this); +} + +void Application::setupEventFilters() +{ + installNativeEventFilter(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); + } + } + } + } +} + +} // namespace + diff --git a/main.h b/main.h new file mode 100644 index 0000000..6bde7f2 --- /dev/null +++ b/main.h @@ -0,0 +1,264 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ + +#ifndef MAIN_H +#define MAIN_H + +#include +#include + +#include +#include +#include +// Qt +#include +#include +#include + +class KPluginMetaData; +class QCommandLineParser; + +namespace KWin +{ + +class Platform; + +class XcbEventFilter : public QAbstractNativeEventFilter +{ +public: + virtual 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) + Q_PROPERTY(KSharedConfigPtr inputConfig READ inputConfig WRITE setInputConfig) +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 + }; + virtual ~Application(); + + void setConfigLock(bool lock); + + KSharedConfigPtr config() const { + return m_config; + } + void setConfig(KSharedConfigPtr config) { + m_config = config; + } + + KSharedConfigPtr kxkbConfig() const { + return m_kxkbConfig; + } + void setKxkbConfig(KSharedConfigPtr config) { + m_kxkbConfig = config; + } + + KSharedConfigPtr inputConfig() const { + return m_inputConfig; + } + void setInputConfig(KSharedConfigPtr config) { + m_inputConfig = 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; + } + +#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; + } + + 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(); + +protected: + Application(OperationMode mode, int &argc, char **argv); + virtual void performStartup() = 0; + + void notifyKSplash(); + void createInput(); + void createWorkspace(); + void createAtoms(); + void createOptions(); + void createCompositor(); + void setupEventFilters(); + 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; + emit x11ConnectionChanged(); + } + void destroyAtoms(); + +protected: + QString m_originalSessionKey; + static int crashes; + +private Q_SLOTS: + void resetCrashesCount(); + +private: + QScopedPointer m_eventFilter; + bool m_configLock; + KSharedConfigPtr m_config; + KSharedConfigPtr m_kxkbConfig; + KSharedConfigPtr m_inputConfig; + 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; +#ifdef KWIN_BUILD_ACTIVITIES + bool m_useKActivities = true; +#endif + Platform *m_platform = nullptr; +}; + +inline static Application *kwinApp() +{ + return static_cast(QCoreApplication::instance()); +} + +} // namespace + +#endif diff --git a/main_wayland.cpp b/main_wayland.cpp new file mode 100644 index 0000000..443499f --- /dev/null +++ b/main_wayland.cpp @@ -0,0 +1,808 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "main_wayland.h" +#include "composite.h" +#include "virtualkeyboard.h" +#include "workspace.h" +#include +// kwin +#include "platform.h" +#include "effects.h" +#include "tabletmodemanager.h" +#include "wayland_server.h" +#include "xcbutils.h" + +// KWayland +#include +#include +// KDE +#include +#include +#include +#include + +// Qt +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// system +#ifdef HAVE_UNISTD_H +#include +#endif // HAVE_UNISTD_H + +#if HAVE_SYS_PRCTL_H +#include +#endif +#if HAVE_SYS_PROCCTL_H +#include +#include +#endif + +#if HAVE_LIBCAP +#include +#endif + +#include + +#include +#include + +namespace KWin +{ + +static void sighandler(int) +{ + QApplication::exit(); +} + +static void readDisplay(int pipe); + +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) + : Application(OperationModeWaylandOnly, argc, argv) +{ +} + +ApplicationWayland::~ApplicationWayland() +{ + if (!waylandServer()) { + return; + } + + if (kwinApp()->platform()) { + kwinApp()->platform()->setOutputsEnabled(false); + } + // need to unload all effects prior to destroying X connection as they might do X calls + if (effects) { + static_cast(effects)->unloadAllEffects(); + } + destroyWorkspace(); + waylandServer()->dispatch(); + disconnect(m_xwaylandFailConnection); + if (x11Connection()) { + Xcb::setInputFocus(XCB_INPUT_FOCUS_POINTER_ROOT); + destroyAtoms(); + emit x11ConnectionAboutToBeDestroyed(); + xcb_disconnect(x11Connection()); + setX11Connection(nullptr); + } + if (m_xwaylandProcess) { + m_xwaylandProcess->terminate(); + while (m_xwaylandProcess->state() != QProcess::NotRunning) { + processEvents(QEventLoop::WaitForMoreEvents); + } + waylandServer()->destroyXWaylandConnection(); + } + 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); +} + +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(); + + if (operationMode() == OperationModeWaylandOnly) { + createCompositor(); + connect(Compositor::self(), &Compositor::sceneCreated, this, &ApplicationWayland::continueStartupWithSceen); + return; + } + createCompositor(); + connect(Compositor::self(), &Compositor::sceneCreated, this, &ApplicationWayland::startXwaylandServer); +} + +void ApplicationWayland::continueStartupWithSceen() +{ + disconnect(Compositor::self(), &Compositor::sceneCreated, this, &ApplicationWayland::continueStartupWithSceen); + startSession(); + createWorkspace(); + notifyKSplash(); +} + +void ApplicationWayland::continueStartupWithX() +{ + createX11Connection(); + xcb_connection_t *c = x11Connection(); + if (!c) { + // about to quit + return; + } + QSocketNotifier *notifier = new QSocketNotifier(xcb_get_file_descriptor(c), QSocketNotifier::Read, this); + auto processXcbEvents = [this, c] { + while (auto event = xcb_poll_for_event(c)) { + long result = 0; + QThread::currentThread()->eventDispatcher()->filterNativeEvent(QByteArrayLiteral("xcb_generic_event_t"), event, &result); + free(event); + } + xcb_flush(c); + }; + connect(notifier, &QSocketNotifier::activated, this, processXcbEvents); + connect(QThread::currentThread()->eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, processXcbEvents); + connect(QThread::currentThread()->eventDispatcher(), &QAbstractEventDispatcher::awake, this, processXcbEvents); + + // create selection owner for WM_S0 - magic X display number expected by XWayland + KSelectionOwner owner("WM_S0", c, x11RootWindow()); + owner.claim(true); + + createAtoms(); + + setupEventFilters(); + + // 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_wayland: an X11 window manager is running on the X11 Display.\n").toLocal8Bit().constData(), stderr); + ::exit(1); + } + + m_environment.insert(QStringLiteral("DISPLAY"), QString::fromUtf8(qgetenv("DISPLAY"))); + + startSession(); + createWorkspace(); + + Xcb::sync(); // Trigger possible errors, there's still a chance to abort + + notifyKSplash(); +} + +void ApplicationWayland::startSession() +{ + if (!m_inputMethodServerToStart.isEmpty()) { + int socket = dup(waylandServer()->createInputMethodConnection()); + if (socket >= 0) { + QProcessEnvironment environment = m_environment; + 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); + auto finishedSignal = static_cast(&QProcess::finished); + connect(p, finishedSignal, this, + [this, p] { + if (waylandServer()) { + waylandServer()->destroyInputMethodConnection(); + } + p->deleteLater(); + } + ); + p->setProcessEnvironment(environment); + p->start(m_inputMethodServerToStart); + p->waitForStarted(); + } + } + + // start session + if (!m_sessionArgument.isEmpty()) { + QProcess *p = new Process(this); + p->setProcessChannelMode(QProcess::ForwardedErrorChannel); + p->setProcessEnvironment(m_environment); + auto finishedSignal = static_cast(&QProcess::finished); + connect(p, finishedSignal, this, &ApplicationWayland::quit); + p->start(m_sessionArgument); + } + // start the applications passed to us as command line arguments + if (!m_applicationsToStart.isEmpty()) { + for (const QString &application: m_applicationsToStart) { + // 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(m_environment); + p->start(application); + } + } +} + +void ApplicationWayland::createX11Connection() +{ + int screenNumber = 0; + xcb_connection_t *c = nullptr; + if (m_xcbConnectionFd == -1) { + c = xcb_connect(nullptr, &screenNumber); + } else { + c = xcb_connect_to_fd(m_xcbConnectionFd, nullptr); + } + if (int error = xcb_connection_has_error(c)) { + std::cerr << "FATAL ERROR: Creating connection to XServer failed: " << error << std::endl; + exit(1); + return; + } + setX11Connection(c); + // we don't support X11 multi-head in Wayland + setX11ScreenNumber(screenNumber); + setX11RootWindow(defaultScreen()->root); +} + +void ApplicationWayland::startXwaylandServer() +{ + disconnect(Compositor::self(), &Compositor::sceneCreated, this, &ApplicationWayland::startXwaylandServer); + int pipeFds[2]; + if (pipe(pipeFds) != 0) { + std::cerr << "FATAL ERROR failed to create pipe to start Xwayland " << std::endl; + exit(1); + return; + } + int sx[2]; + if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sx) < 0) { + std::cerr << "FATAL ERROR: failed to open socket to open XCB connection" << std::endl; + exit(1); + return; + } + int fd = dup(sx[1]); + if (fd < 0) { + std::cerr << "FATAL ERROR: failed to open socket to open XCB connection" << std::endl; + exit(20); + return; + } + + const int waylandSocket = waylandServer()->createXWaylandConnection(); + if (waylandSocket == -1) { + std::cerr << "FATAL ERROR: failed to open socket for Xwayland" << std::endl; + exit(1); + return; + } + const int wlfd = dup(waylandSocket); + if (wlfd < 0) { + std::cerr << "FATAL ERROR: failed to open socket for Xwayland" << std::endl; + exit(20); + return; + } + + m_xcbConnectionFd = sx[0]; + + m_xwaylandProcess = new Process(kwinApp()); + m_xwaylandProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel); + m_xwaylandProcess->setProgram(QStringLiteral("Xwayland")); + QProcessEnvironment env = m_environment; + env.insert("WAYLAND_SOCKET", QByteArray::number(wlfd)); + env.insert("EGL_PLATFORM", QByteArrayLiteral("DRM")); + m_xwaylandProcess->setProcessEnvironment(env); + m_xwaylandProcess->setArguments({QStringLiteral("-displayfd"), + QString::number(pipeFds[1]), + QStringLiteral("-rootless"), + QStringLiteral("-wm"), + QString::number(fd)}); + m_xwaylandFailConnection = connect(m_xwaylandProcess, static_cast(&QProcess::error), this, + [] (QProcess::ProcessError error) { + if (error == QProcess::FailedToStart) { + std::cerr << "FATAL ERROR: failed to start Xwayland" << std::endl; + } else { + std::cerr << "FATAL ERROR: Xwayland failed, going to exit now" << std::endl; + } + exit(1); + } + ); + const int xDisplayPipe = pipeFds[0]; + connect(m_xwaylandProcess, &QProcess::started, this, + [this, xDisplayPipe] { + QFutureWatcher *watcher = new QFutureWatcher(this); + QObject::connect(watcher, &QFutureWatcher::finished, this, &ApplicationWayland::continueStartupWithX, Qt::QueuedConnection); + QObject::connect(watcher, &QFutureWatcher::finished, watcher, &QFutureWatcher::deleteLater, Qt::QueuedConnection); + watcher->setFuture(QtConcurrent::run(readDisplay, xDisplayPipe)); + } + ); + m_xwaylandProcess->start(); + close(pipeFds[1]); +} + +static void readDisplay(int pipe) +{ + QFile readPipe; + if (!readPipe.open(pipe, QIODevice::ReadOnly)) { + std::cerr << "FATAL ERROR failed to open pipe to start X Server" << std::endl; + exit(1); + } + QByteArray displayNumber = readPipe.readLine(); + + displayNumber.prepend(QByteArray(":")); + displayNumber.remove(displayNumber.size() -1, 1); + std::cout << "X-Server started on display " << displayNumber.constData() << std::endl; + + setenv("DISPLAY", displayNumber.constData(), true); + + // close our pipe + close(pipe); +} + +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")))) { + // 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_DisableHighDpiScaling); + 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("height")); + 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.")); + 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 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 < 1) { + 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::InitalizationFlags flags; + if (parser.isSet(screenLockerOption)) { + flags = KWin::WaylandServer::InitalizationFlag::LockScreen; + } + 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..31b7ebd --- /dev/null +++ b/main_wayland.h @@ -0,0 +1,81 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_MAIN_WAYLAND_H +#define KWIN_MAIN_WAYLAND_H +#include "main.h" +#include + +class QProcess; + +namespace KWin +{ + +class ApplicationWayland : public Application +{ + Q_OBJECT +public: + ApplicationWayland(int &argc, char **argv); + virtual ~ApplicationWayland(); + + 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) { + 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 createX11Connection(); + void continueStartupWithScreens(); + void continueStartupWithSceen(); + void continueStartupWithX(); + void startXwaylandServer(); + void startSession(); + + bool m_startXWayland = false; + int m_xcbConnectionFd = -1; + QStringList m_applicationsToStart; + QString m_inputMethodServerToStart; + QProcess *m_xwaylandProcess = nullptr; + QMetaObject::Connection m_xwaylandFailConnection; + QProcessEnvironment m_environment; + QString m_sessionArgument; +}; + +} + +#endif diff --git a/main_x11.cpp b/main_x11.cpp new file mode 100644 index 0000000..765caef --- /dev/null +++ b/main_x11.cpp @@ -0,0 +1,469 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "main_x11.h" +#include +// kwin +#include "platform.h" +#include "sm.h" +#include "workspace.h" +#include "xcbutils.h" + +// KDE +#include +#include +#include +#include +#include +#include +// Qt +#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; +}; + +//************************************ +// KWinSelectionOwner +//************************************ + +KWinSelectionOwner::KWinSelectionOwner(int screen_P) + : KSelectionOwner(make_selection_atom(screen_P), screen_P) +{ +} + +xcb_atom_t KWinSelectionOwner::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; +} + +void KWinSelectionOwner::getAtoms() +{ + 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; + } + } +} + +void KWinSelectionOwner::replyTargets(xcb_atom_t property_P, xcb_window_t requestor_P) +{ + 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); +} + +bool KWinSelectionOwner::genericReply(xcb_atom_t target_P, xcb_atom_t property_P, xcb_window_t requestor_P) +{ + 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; +} + +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() +{ + 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(); +} + +void ApplicationX11::performStartup() +{ + crashChecking(); + + if (Application::x11ScreenNumber() == -1) { + Application::setX11ScreenNumber(QX11Info::appScreen()); + } + + // QSessionManager for some reason triggers a very early commitDataRequest + // and updates the key - before we create the workspace and load the session + // data -> store and pass to the workspace constructor + m_originalSessionKey = sessionKey(); + + 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]{ + setupEventFilters(); + // 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(); + } + ); + 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 (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.toAscii().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::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 + +extern "C" +KWIN_EXPORT int kdemain(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" + + QString envir; + 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 + envir.sprintf("DISPLAY=%s.%d", display_name.data(), KWin::Application::x11ScreenNumber()); + + if (putenv(strdup(envir.toAscii().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"); + QCoreApplication::setAttribute(Qt::AA_DisableHighDpiScaling); + + KWin::ApplicationX11 a(argc, argv); + a.setupTranslator(); + + KWin::Application::createAboutData(); + KQuickAddons::QtQuickSettings::init(); + + 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(); + + KWin::SessionSaveDoneHelper helper; + Q_UNUSED(helper); // The sessionsavedonehelper opens a side channel to the smserver, + // listens for events and talks to it, so it needs to be created. + return a.exec(); +} diff --git a/main_x11.h b/main_x11.h new file mode 100644 index 0000000..2d8e4c6 --- /dev/null +++ b/main_x11.h @@ -0,0 +1,70 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_MAIN_X11_H +#define KWIN_MAIN_X11_H +#include "main.h" + +namespace KWin +{ + +class KWinSelectionOwner + : public KSelectionOwner +{ + Q_OBJECT +public: + explicit KWinSelectionOwner(int screen); +protected: + virtual bool genericReply(xcb_atom_t target, xcb_atom_t property, xcb_window_t requestor); + virtual void replyTargets(xcb_atom_t property, xcb_window_t requestor); + virtual void getAtoms(); +private: + xcb_atom_t make_selection_atom(int screen); + static xcb_atom_t xa_version; +}; + +class ApplicationX11 : public Application +{ + Q_OBJECT +public: + ApplicationX11(int &argc, char **argv); + virtual ~ApplicationX11(); + + 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(); + + static void crashHandler(int signal); + + QScopedPointer owner; + bool m_replace; +}; + +} + +#endif diff --git a/manage.cpp b/manage.cpp new file mode 100644 index 0000000..9a264cf --- /dev/null +++ b/manage.cpp @@ -0,0 +1,792 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ + +// This file contains things relevant to handling incoming events. + +#include "client.h" + +#include + +#ifdef KWIN_BUILD_ACTIVITIES +#include "activities.h" +#endif +#include "cursor.h" +#include "rules.h" +#include "group.h" +#include "netinfo.h" +#include "screens.h" +#include "workspace.h" +#include "xcbutils.h" + +#include + +namespace KWin +{ + +/** + * Manages the clients. This means handling the very first maprequest: + * reparenting, initial geometry, initial state, placement, etc. + * Returns false if KWin is not going to manage this window. + */ +bool Client::manage(xcb_window_t w, bool isMapped) +{ + StackingUpdatesBlocker stacking_blocker(workspace()); + + Xcb::WindowAttributes attr(w); + Xcb::WindowGeometry windowGeometry(w); + if (attr.isNull() || windowGeometry.isNull()) { + return false; + } + + // From this place on, manage() must not return false + blockGeometryUpdates(); + setPendingGeometryUpdate(PendingGeometryForced); // Force update when finishing with geometry changes + + embedClient(w, attr->visual, attr->colormap, windowGeometry->depth); + + m_visual = attr->visual; + bit_depth = windowGeometry->depth; + + // SELI TODO: Order all these things in some sane manner + + const NET::Properties properties = + NET::WMDesktop | + NET::WMState | + NET::WMWindowType | + NET::WMStrut | + NET::WMName | + NET::WMIconGeometry | + NET::WMIcon | + NET::WMPid | + NET::WMIconName; + const NET::Properties2 properties2 = + NET::WM2BlockCompositing | + NET::WM2WindowClass | + NET::WM2WindowRole | + NET::WM2UserTime | + NET::WM2StartupId | + NET::WM2ExtendedStrut | + NET::WM2Opacity | + NET::WM2FullscreenMonitors | + NET::WM2FrameOverlap | + NET::WM2GroupLeader | + NET::WM2Urgency | + NET::WM2Input | + NET::WM2Protocols | + NET::WM2InitialMappingState | + NET::WM2IconPixmap | + NET::WM2OpaqueRegion | + NET::WM2DesktopFileName; + + auto wmClientLeaderCookie = fetchWmClientLeader(); + auto skipCloseAnimationCookie = fetchSkipCloseAnimation(); + auto gtkFrameExtentsCookie = fetchGtkFrameExtents(); + auto showOnScreenEdgeCookie = fetchShowOnScreenEdge(); + auto colorSchemeCookie = fetchColorScheme(); + auto firstInTabBoxCookie = fetchFirstInTabBox(); + auto transientCookie = fetchTransient(); + auto activitiesCookie = fetchActivities(); + auto applicationMenuServiceNameCookie = fetchApplicationMenuServiceName(); + auto applicationMenuObjectPathCookie = fetchApplicationMenuObjectPath(); + + m_geometryHints.init(window()); + m_motif.init(window()); + info = new WinInfo(this, m_client, rootWindow(), properties, properties2); + + if (isDesktop() && bit_depth == 32) { + // force desktop windows to be opaque. It's a desktop after all, there is no window below + bit_depth = 24; + } + + // If it's already mapped, ignore hint + bool init_minimize = !isMapped && (info->initialMappingState() == NET::Iconic); + + m_colormap = attr->colormap; + + getResourceClass(); + readWmClientLeader(wmClientLeaderCookie); + getWmClientMachine(); + getSyncCounter(); + // First only read the caption text, so that setupWindowRules() can use it for matching, + // and only then really set the caption using setCaption(), which checks for duplicates etc. + // and also relies on rules already existing + cap_normal = readName(); + setupWindowRules(false); + setCaption(cap_normal, true); + + if (Xcb::Extensions::self()->isShapeAvailable()) + xcb_shape_select_input(connection(), window(), true); + detectShape(window()); + readGtkFrameExtents(gtkFrameExtentsCookie); + detectNoBorder(); + fetchIconicName(); + + // Needs to be done before readTransient() because of reading the group + checkGroup(); + updateUrgency(); + updateAllowedActions(); // Group affects isMinimizable() + + setModal((info->state() & NET::Modal) != 0); // Needs to be valid before handling groups + readTransientProperty(transientCookie); + setDesktopFileName(rules()->checkDesktopFile(QByteArray(info->desktopFileName()), true).toUtf8()); + getIcons(); + connect(this, &Client::desktopFileNameChanged, this, &Client::getIcons); + + m_geometryHints.read(); + getMotifHints(); + getWmOpaqueRegion(); + readSkipCloseAnimation(skipCloseAnimationCookie); + + // TODO: Try to obey all state information from info->state() + + setOriginalSkipTaskbar((info->state() & NET::SkipTaskbar) != 0); + setSkipPager((info->state() & NET::SkipPager) != 0); + setSkipSwitcher((info->state() & NET::SkipSwitcher) != 0); + readFirstInTabBox(firstInTabBoxCookie); + + setupCompositing(); + + KStartupInfoId asn_id; + KStartupInfoData asn_data; + bool asn_valid = workspace()->checkStartupNotification(window(), asn_id, asn_data); + + // Make sure that the input window is created before we update the stacking order + updateInputWindow(); + + workspace()->updateClientLayer(this); + + SessionInfo* session = workspace()->takeSessionInfo(this); + if (session) { + init_minimize = session->minimized; + noborder = session->noBorder; + } + + setShortcut(rules()->checkShortcut(session ? session->shortcut : QString(), true)); + + init_minimize = rules()->checkMinimize(init_minimize, !isMapped); + noborder = rules()->checkNoBorder(noborder, !isMapped); + + readActivities(activitiesCookie); + + // Initial desktop placement + int desk = 0; + if (session) { + desk = session->desktop; + if (session->onAllDesktops) + desk = NET::OnAllDesktops; + setOnActivities(session->activities); + } else { + // If this window is transient, ensure that it is opened on the + // same window as its parent. this is necessary when an application + // starts up on a different desktop than is currently displayed + if (isTransient()) { + auto mainclients = mainClients(); + bool on_current = false; + bool on_all = false; + AbstractClient* maincl = nullptr; + // This is slightly duplicated from Placement::placeOnMainWindow() + for (auto it = mainclients.constBegin(); + it != mainclients.constEnd(); + ++it) { + if (mainclients.count() > 1 && // A group-transient + (*it)->isSpecialWindow() && // Don't consider toolbars etc when placing + !(info->state() & NET::Modal)) // except when it's modal (blocks specials as well) + continue; + maincl = *it; + if ((*it)->isOnCurrentDesktop()) + on_current = true; + if ((*it)->isOnAllDesktops()) + on_all = true; + } + if (on_all) + desk = NET::OnAllDesktops; + else if (on_current) + desk = VirtualDesktopManager::self()->current(); + else if (maincl != NULL) + desk = maincl->desktop(); + + if (maincl) + setOnActivities(maincl->activities()); + } else { // a transient shall appear on its leader and not drag that around + if (info->desktop()) + desk = info->desktop(); // Window had the initial desktop property, force it + if (desktop() == 0 && asn_valid && asn_data.desktop() != 0) + desk = asn_data.desktop(); + } +#ifdef KWIN_BUILD_ACTIVITIES + if (Activities::self() && !isMapped && !noborder && isNormalWindow() && !activitiesDefined) { + //a new, regular window, when we're not recovering from a crash, + //and it hasn't got an activity. let's try giving it the current one. + //TODO: decide whether to keep this before the 4.6 release + //TODO: if we are keeping it (at least as an option), replace noborder checking + //with a public API for setting windows to be on all activities. + //something like KWindowSystem::setOnAllActivities or + //KActivityConsumer::setOnAllActivities + setOnActivity(Activities::self()->current(), true); + } +#endif + } + + if (desk == 0) // Assume window wants to be visible on the current desktop + desk = isDesktop() ? static_cast(NET::OnAllDesktops) : VirtualDesktopManager::self()->current(); + desk = rules()->checkDesktop(desk, !isMapped); + if (desk != NET::OnAllDesktops) // Do range check + desk = qBound(1, desk, static_cast(VirtualDesktopManager::self()->count())); + setDesktop(desk); + info->setDesktop(desk); + workspace()->updateOnAllDesktopsOfTransients(this); // SELI TODO + //onAllDesktopsChange(); // Decoration doesn't exist here yet + + QString activitiesList; + activitiesList = rules()->checkActivity(activitiesList, !isMapped); + if (!activitiesList.isEmpty()) + setOnActivities(activitiesList.split(QStringLiteral(","))); + + QRect geom(windowGeometry.rect()); + bool placementDone = false; + + if (session) + geom = session->geometry; + + QRect area; + bool partial_keep_in_area = isMapped || session; + if (isMapped || session) { + area = workspace()->clientArea(FullArea, geom.center(), desktop()); + checkOffscreenPosition(&geom, area); + } else { + int screen = asn_data.xinerama() == -1 ? screens()->current() : asn_data.xinerama(); + screen = rules()->checkScreen(screen, !isMapped); + area = workspace()->clientArea(PlacementArea, screens()->geometry(screen).center(), desktop()); + } + + if (int type = checkFullScreenHack(geom)) { + fullscreen_mode = FullScreenHack; + if (rules()->checkStrictGeometry(false)) { + geom = type == 2 // 1 = It's xinerama-aware fullscreen hack, 2 = It's full area + ? workspace()->clientArea(FullArea, geom.center(), desktop()) + : workspace()->clientArea(ScreenArea, geom.center(), desktop()); + } else + geom = workspace()->clientArea(FullScreenArea, geom.center(), desktop()); + placementDone = true; + } + + if (isDesktop()) + // KWin doesn't manage desktop windows + placementDone = true; + + bool usePosition = false; + if (isMapped || session || placementDone) + placementDone = true; // Use geometry + else if (isTransient() && !isUtility() && !isDialog() && !isSplash()) + usePosition = true; + else if (isTransient() && !hasNETSupport()) + usePosition = true; + else if (isDialog() && hasNETSupport()) { + // If the dialog is actually non-NETWM transient window, don't try to apply placement to it, + // it breaks with too many things (xmms, display) + if (mainClients().count() >= 1) { +#if 1 + // #78082 - Ok, it seems there are after all some cases when an application has a good + // reason to specify a position for its dialog. Too bad other WMs have never bothered + // with placement for dialogs, so apps always specify positions for their dialogs, + // including such silly positions like always centered on the screen or under mouse. + // Using ignoring requested position in window-specific settings helps, and now + // there's also _NET_WM_FULL_PLACEMENT. + usePosition = true; +#else + ; // Force using placement policy +#endif + } else + usePosition = true; + } else if (isSplash()) + ; // Force using placement policy + else + usePosition = true; + if (!rules()->checkIgnoreGeometry(!usePosition, true)) { + if (m_geometryHints.hasPosition()) { + placementDone = true; + // Disobey xinerama placement option for now (#70943) + area = workspace()->clientArea(PlacementArea, geom.center(), desktop()); + } + } + //if ( true ) // Size is always obeyed for now, only with constraints applied + // if (( xSizeHint.flags & USSize ) || ( xSizeHint.flags & PSize )) + // { + // // Keep in mind that we now actually have a size :-) + // } + + if (m_geometryHints.hasMaxSize()) + geom.setSize(geom.size().boundedTo( + rules()->checkMaxSize(m_geometryHints.maxSize()))); + if (m_geometryHints.hasMinSize()) + geom.setSize(geom.size().expandedTo( + rules()->checkMinSize(m_geometryHints.minSize()))); + + if (isMovable() && (geom.x() > area.right() || geom.y() > area.bottom())) + placementDone = false; // Weird, do not trust. + + if (placementDone) + move(geom.x(), geom.y()); // Before gravitating + + // Create client group if the window will have a decoration + bool dontKeepInArea = false; + setTabGroup(NULL); + if (!noBorder() && false) { + const bool autogrouping = rules()->checkAutogrouping(options->isAutogroupSimilarWindows()); + const bool autogroupInFg = rules()->checkAutogroupInForeground(options->isAutogroupInForeground()); + // Automatically add to previous groups on session restore + if (session && session->tabGroupClient && !workspace()->hasClient(session->tabGroupClient)) + session->tabGroupClient = NULL; + if (session && session->tabGroupClient && session->tabGroupClient != this) { + tabBehind(session->tabGroupClient, autogroupInFg); + } else if (isMapped && autogrouping) { + // If the window is already mapped (Restarted KWin) add any windows that already have the + // same geometry to the same client group. (May incorrectly handle maximized windows) + foreach (Client *other, workspace()->clientList()) { + if (other->maximizeMode() != MaximizeFull && + geom == QRect(other->pos(), other->clientSize()) && + desk == other->desktop() && activities() == other->activities()) { + + tabBehind(other, autogroupInFg); + break; + + } + } + } + if (!(tabGroup() || isMapped || session)) { + // Attempt to automatically group similar windows + Client* similar = findAutogroupCandidate(); + if (similar && !similar->noBorder()) { + if (autogroupInFg) { + similar->setDesktop(desk); // can happen when grouping by id. ... + similar->setMinimized(false); // ... or anyway - still group, but "here" and visible + } + if (!similar->isMinimized()) { // do not attempt to tab in background of a hidden group + geom = QRect(similar->pos() + similar->clientPos(), similar->clientSize()); + updateDecoration(false); + if (tabBehind(similar, autogroupInFg)) { + // Don't move entire group + geom = QRect(similar->pos() + similar->clientPos(), similar->clientSize()); + placementDone = true; + dontKeepInArea = true; + } + } + } + } + } + + readColorScheme(colorSchemeCookie); + + readApplicationMenuServiceName(applicationMenuServiceNameCookie); + readApplicationMenuObjectPath(applicationMenuObjectPathCookie); + + updateDecoration(false); // Also gravitates + // TODO: Is CentralGravity right here, when resizing is done after gravitating? + plainResize(rules()->checkSize(sizeForClientSize(geom.size()), !isMapped)); + + QPoint forced_pos = rules()->checkPosition(invalidPoint, !isMapped); + if (forced_pos != invalidPoint) { + move(forced_pos); + placementDone = true; + // Don't keep inside workarea if the window has specially configured position + partial_keep_in_area = true; + area = workspace()->clientArea(FullArea, geom.center(), desktop()); + } + if (!placementDone) { + // Placement needs to be after setting size + Placement::self()->place(this, area); + dontKeepInArea = true; + placementDone = true; + } + + // bugs #285967, #286146, #183694 + // geometry() now includes the requested size and the decoration and is at the correct screen/position (hopefully) + // Maximization for oversized windows must happen NOW. + // If we effectively pass keepInArea(), the window will resizeWithChecks() - i.e. constrained + // to the combo of all screen MINUS all struts on the edges + // If only one screen struts, this will affect screens as a side-effect, the window is artificailly shrinked + // below the screen size and as result no more maximized what breaks KMainWindow's stupid width+1, height+1 hack + // TODO: get KMainWindow a correct state storage what will allow to store the restore size as well. + + if (!session) { // has a better handling of this + geom_restore = geometry(); // Remember restore geometry + if (isMaximizable() && (width() >= area.width() || height() >= area.height())) { + // Window is too large for the screen, maximize in the + // directions necessary + const QSize ss = workspace()->clientArea(ScreenArea, area.center(), desktop()).size(); + const QRect fsa = workspace()->clientArea(FullArea, geom.center(), desktop()); + const QSize cs = clientSize(); + int pseudo_max = ((info->state() & NET::MaxVert) ? MaximizeVertical : 0) | + ((info->state() & NET::MaxHoriz) ? MaximizeHorizontal : 0); + if (width() >= area.width()) + pseudo_max |= MaximizeHorizontal; + if (height() >= area.height()) + pseudo_max |= MaximizeVertical; + + // heuristics: + // if decorated client is smaller than the entire screen, the user might want to move it around (multiscreen) + // in this case, if the decorated client is bigger than the screen (+1), we don't take this as an + // attempt for maximization, but just constrain the size (the window simply wants to be bigger) + // NOTICE + // i intended a second check on cs < area.size() ("the managed client ("minus border") is smaller + // than the workspace") but gtk / gimp seems to store it's size including the decoration, + // thus a former maximized window wil become non-maximized + bool keepInFsArea = false; + if (width() < fsa.width() && (cs.width() > ss.width()+1)) { + pseudo_max &= ~MaximizeHorizontal; + keepInFsArea = true; + } + if (height() < fsa.height() && (cs.height() > ss.height()+1)) { + pseudo_max &= ~MaximizeVertical; + keepInFsArea = true; + } + + if (pseudo_max != MaximizeRestore) { + maximize((MaximizeMode)pseudo_max); + // from now on, care about maxmode, since the maximization call will override mode for fix aspects + dontKeepInArea |= (max_mode == MaximizeFull); + geom_restore = QRect(); // Use placement when unmaximizing ... + if (!(max_mode & MaximizeVertical)) { + geom_restore.setY(y()); // ...but only for horizontal direction + geom_restore.setHeight(height()); + } + if (!(max_mode & MaximizeHorizontal)) { + geom_restore.setX(x()); // ...but only for vertical direction + geom_restore.setWidth(width()); + } + } + if (keepInFsArea) + keepInArea(fsa, partial_keep_in_area); + } + } + + if ((!isSpecialWindow() || isToolbar()) && isMovable() && !dontKeepInArea) + keepInArea(area, partial_keep_in_area); + + updateShape(); + + // CT: Extra check for stupid jdk 1.3.1. But should make sense in general + // if client has initial state set to Iconic and is transient with a parent + // window that is not Iconic, set init_state to Normal + if (init_minimize && isTransient()) { + auto mainclients = mainClients(); + for (auto it = mainclients.constBegin(); + it != mainclients.constEnd(); + ++it) + if ((*it)->isShown(true)) + init_minimize = false; // SELI TODO: Even e.g. for NET::Utility? + } + // If a dialog is shown for minimized window, minimize it too + if (!init_minimize && isTransient() && mainClients().count() > 0 && !workspace()->sessionSaving()) { + bool visible_parent = false; + // Use allMainClients(), to include also main clients of group transients + // that have been optimized out in Client::checkGroupTransients() + auto mainclients = allMainClients(); + for (auto it = mainclients.constBegin(); + it != mainclients.constEnd(); + ++it) + if ((*it)->isShown(true)) + visible_parent = true; + if (!visible_parent) { + init_minimize = true; + demandAttention(); + } + } + + if (init_minimize) + minimize(true); // No animation + + // Other settings from the previous session + if (session) { + // Session restored windows are not considered to be new windows WRT rules, + // I.e. obey only forcing rules + setKeepAbove(session->keepAbove); + setKeepBelow(session->keepBelow); + setOriginalSkipTaskbar(session->skipTaskbar); + setSkipPager(session->skipPager); + setSkipSwitcher(session->skipSwitcher); + setShade(session->shaded ? ShadeNormal : ShadeNone); + setOpacity(session->opacity); + geom_restore = session->restore; + if (session->maximized != MaximizeRestore) { + maximize(MaximizeMode(session->maximized)); + } + if (session->fullscreen == FullScreenHack) + ; // Nothing, this should be already set again above + else if (session->fullscreen != FullScreenNone) { + setFullScreen(true, false); + geom_fs_restore = session->fsrestore; + } + checkOffscreenPosition(&geom_restore, area); + checkOffscreenPosition(&geom_fs_restore, area); + } else { + // Window may want to be maximized + // done after checking that the window isn't larger than the workarea, so that + // the restore geometry from the checks above takes precedence, and window + // isn't restored larger than the workarea + MaximizeMode maxmode = static_cast( + ((info->state() & NET::MaxVert) ? MaximizeVertical : 0) | + ((info->state() & NET::MaxHoriz) ? MaximizeHorizontal : 0)); + MaximizeMode forced_maxmode = rules()->checkMaximize(maxmode, !isMapped); + + // Either hints were set to maximize, or is forced to maximize, + // or is forced to non-maximize and hints were set to maximize + if (forced_maxmode != MaximizeRestore || maxmode != MaximizeRestore) + maximize(forced_maxmode); + + // Read other initial states + setShade(rules()->checkShade(info->state() & NET::Shaded ? ShadeNormal : ShadeNone, !isMapped)); + setKeepAbove(rules()->checkKeepAbove(info->state() & NET::KeepAbove, !isMapped)); + setKeepBelow(rules()->checkKeepBelow(info->state() & NET::KeepBelow, !isMapped)); + setOriginalSkipTaskbar(rules()->checkSkipTaskbar(info->state() & NET::SkipTaskbar, !isMapped)); + setSkipPager(rules()->checkSkipPager(info->state() & NET::SkipPager, !isMapped)); + setSkipSwitcher(rules()->checkSkipSwitcher(info->state() & NET::SkipSwitcher, !isMapped)); + if (info->state() & NET::DemandsAttention) + demandAttention(); + if (info->state() & NET::Modal) + setModal(true); + if (fullscreen_mode != FullScreenHack) + setFullScreen(rules()->checkFullScreen(info->state() & NET::FullScreen, !isMapped), false); + } + + updateAllowedActions(true); + + // Set initial user time directly + m_userTime = readUserTimeMapTimestamp(asn_valid ? &asn_id : NULL, asn_valid ? &asn_data : NULL, session); + group()->updateUserTime(m_userTime); // And do what Client::updateUserTime() does + + // This should avoid flicker, because real restacking is done + // only after manage() finishes because of blocking, but the window is shown sooner + m_frame.lower(); + if (session && session->stackingOrder != -1) { + sm_stacking_order = session->stackingOrder; + workspace()->restoreSessionStackingOrder(this); + } + + if (compositing()) + // Sending ConfigureNotify is done when setting mapping state below, + // Getting the first sync response means window is ready for compositing + sendSyncRequest(); + else + ready_for_painting = true; // set to true in case compositing is turned on later. bug #160393 + + if (isShown(true)) { + bool allow; + if (session) + allow = session->active && + (!workspace()->wasUserInteraction() || workspace()->activeClient() == NULL || + workspace()->activeClient()->isDesktop()); + else + allow = workspace()->allowClientActivation(this, userTime(), false); + + // If session saving, force showing new windows (i.e. "save file?" dialogs etc.) + // also force if activation is allowed + if( !isOnCurrentDesktop() && !isMapped && !session && ( allow || workspace()->sessionSaving() )) + VirtualDesktopManager::self()->setCurrent( desktop()); + + // If the window is on an inactive activity during session saving, temporarily force it to show. + if( !isMapped && !session && workspace()->sessionSaving() && !isOnCurrentActivity()) { + setSessionActivityOverride( true ); + foreach( AbstractClient* c, mainClients()) { + if (Client *mc = dynamic_cast(c)) { + mc->setSessionActivityOverride(true); + } + } + } + + if (isOnCurrentDesktop() && !isMapped && !allow && (!session || session->stackingOrder < 0)) + workspace()->restackClientUnderActive(this); + + updateVisibility(); + + if (!isMapped) { + if (allow && isOnCurrentDesktop()) { + if (!isSpecialWindow()) + if (options->focusPolicyIsReasonable() && wantsTabFocus()) + workspace()->requestFocus(this); + } else if (!session && !isSpecialWindow()) + demandAttention(); + } + } else + updateVisibility(); + assert(mapping_state != Withdrawn); + m_managed = true; + blockGeometryUpdates(false); + + if (m_userTime == XCB_TIME_CURRENT_TIME || m_userTime == -1U) { + // No known user time, set something old + m_userTime = xTime() - 1000000; + if (m_userTime == XCB_TIME_CURRENT_TIME || m_userTime == -1U) // Let's be paranoid + m_userTime = xTime() - 1000000 + 10; + } + + //sendSyntheticConfigureNotify(); // Done when setting mapping state + + delete session; + + discardTemporaryRules(); + applyWindowRules(); // Just in case + RuleBook::self()->discardUsed(this, false); // Remove ApplyNow rules + updateWindowRules(Rules::All); // Was blocked while !isManaged() + + setBlockingCompositing(info->isBlockingCompositing()); + readShowOnScreenEdge(showOnScreenEdgeCookie); + + // TODO: there's a small problem here - isManaged() depends on the mapping state, + // but this client is not yet in Workspace's client list at this point, will + // be only done in addClient() + emit clientManaging(this); + return true; +} + +// Called only from manage() +void Client::embedClient(xcb_window_t w, xcb_visualid_t visualid, xcb_colormap_t colormap, uint8_t depth) +{ + assert(m_client == XCB_WINDOW_NONE); + assert(frameId() == XCB_WINDOW_NONE); + assert(m_wrapper == XCB_WINDOW_NONE); + m_client.reset(w, false); + + const uint32_t zero_value = 0; + + xcb_connection_t *conn = connection(); + + // We don't want the window to be destroyed when we quit + xcb_change_save_set(conn, XCB_SET_MODE_INSERT, m_client); + + m_client.selectInput(zero_value); + m_client.unmap(); + m_client.setBorderWidth(zero_value); + + // Note: These values must match the order in the xcb_cw_t enum + const uint32_t cw_values[] = { + 0, // back_pixmap + 0, // border_pixel + colormap, // colormap + Cursor::x11Cursor(Qt::ArrowCursor) + }; + + const uint32_t cw_mask = XCB_CW_BACK_PIXMAP | XCB_CW_BORDER_PIXEL | + XCB_CW_COLORMAP | XCB_CW_CURSOR; + + const uint32_t common_event_mask = XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE | + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW | + XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | + XCB_EVENT_MASK_BUTTON_MOTION | XCB_EVENT_MASK_POINTER_MOTION | + XCB_EVENT_MASK_KEYMAP_STATE | + XCB_EVENT_MASK_FOCUS_CHANGE | + XCB_EVENT_MASK_EXPOSURE | + XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT; + + const uint32_t frame_event_mask = common_event_mask | XCB_EVENT_MASK_PROPERTY_CHANGE | XCB_EVENT_MASK_VISIBILITY_CHANGE; + const uint32_t wrapper_event_mask = common_event_mask | XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY; + + const uint32_t client_event_mask = XCB_EVENT_MASK_FOCUS_CHANGE | XCB_EVENT_MASK_PROPERTY_CHANGE | + XCB_EVENT_MASK_COLOR_MAP_CHANGE | + XCB_EVENT_MASK_ENTER_WINDOW | XCB_EVENT_MASK_LEAVE_WINDOW | + XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE; + + // Create the frame window + xcb_window_t frame = xcb_generate_id(conn); + xcb_create_window(conn, depth, frame, rootWindow(), 0, 0, 1, 1, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, visualid, cw_mask, cw_values); + m_frame.reset(frame); + + setWindowHandles(m_client); + + // Create the wrapper window + xcb_window_t wrapperId = xcb_generate_id(conn); + xcb_create_window(conn, depth, wrapperId, frame, 0, 0, 1, 1, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, visualid, cw_mask, cw_values); + m_wrapper.reset(wrapperId); + + m_client.reparent(m_wrapper); + + // We could specify the event masks when we create the windows, but the original + // Xlib code didn't. Let's preserve that behavior here for now so we don't end up + // receiving any unexpected events from the wrapper creation or the reparenting. + m_frame.selectInput(frame_event_mask); + m_wrapper.selectInput(wrapper_event_mask); + m_client.selectInput(client_event_mask); + + updateMouseGrab(); +} + +// To accept "mainwindow#1" to "mainwindow#2" +static QByteArray truncatedWindowRole(QByteArray a) +{ + int i = a.indexOf('#'); + if (i == -1) + return a; + QByteArray b(a); + b.truncate(i); + return b; +} + +Client* Client::findAutogroupCandidate() const +{ + // Attempt to find a similar window to the input. If we find multiple possibilities that are in + // different groups then ignore all of them. This function is for automatic window grouping. + Client *found = NULL; + + // See if the window has a group ID to match with + QString wGId = rules()->checkAutogroupById(QString()); + if (!wGId.isEmpty()) { + foreach (Client *c, workspace()->clientList()) { + if (activities() != c->activities()) + continue; // don't cross activities + if (wGId == c->rules()->checkAutogroupById(QString())) { + if (found && found->tabGroup() != c->tabGroup()) { // We've found two, ignore both + found = NULL; + break; // Continue to the next test + } + found = c; + } + } + if (found) + return found; + } + + // If this is a transient window don't take a guess + if (isTransient()) + return NULL; + + // If we don't have an ID take a guess + if (rules()->checkAutogrouping(options->isAutogroupSimilarWindows())) { + QByteArray wRole = truncatedWindowRole(windowRole()); + foreach (Client *c, workspace()->clientList()) { + if (desktop() != c->desktop() || activities() != c->activities()) + continue; + QByteArray wRoleB = truncatedWindowRole(c->windowRole()); + if (resourceClass() == c->resourceClass() && // Same resource class + wRole == wRoleB && // Same window role + c->isNormalWindow()) { // Normal window TODO: Can modal windows be "normal"? + if (found && found->tabGroup() != c->tabGroup()) // We've found two, ignore both + return NULL; + found = c; + } + } + } + + return found; +} + +} // namespace diff --git a/modifier_only_shortcuts.cpp b/modifier_only_shortcuts.cpp new file mode 100644 index 0000000..cb47df4 --- /dev/null +++ b/modifier_only_shortcuts.cpp @@ -0,0 +1,102 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016, 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "modifier_only_shortcuts.h" +#include "input_event.h" +#include "options.h" +#include "screenlockerwatcher.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()->globalShortcutsDisabled()) { + if (m_modifier != Qt::NoModifier) { + const auto list = options->modifierOnlyDBusShortcut(m_modifier); + if (list.size() >= 4) { + 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..f4b7446 --- /dev/null +++ b/modifier_only_shortcuts.h @@ -0,0 +1,56 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016, 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..1df4088 --- /dev/null +++ b/moving_client_x11_filter.cpp @@ -0,0 +1,62 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#include "moving_client_x11_filter.h" +#include "client.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()->getMovingClient()); + 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..617760f --- /dev/null +++ b/moving_client_x11_filter.h @@ -0,0 +1,38 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..2838060 --- /dev/null +++ b/netinfo.cpp @@ -0,0 +1,304 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +// own +#include "netinfo.h" +// kwin +#include "client.h" +#include "rootinfo_filter.h" +#include "virtualdesktops.h" +#include "workspace.h" +// Qt +#include + +namespace KWin +{ +extern int screen_number; + +RootInfo *RootInfo::s_self = NULL; + +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::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; +#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 = NULL; + 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 (Client* c = workspace->findClient(Predicate::WindowMatch, w)) { + if (timestamp == CurrentTime) + 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 + Client* 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 != None + && (c2 = workspace->findClient(Predicate::WindowMatch, active_window)) != NULL + && 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 (Client* c = Workspace::self()->findClient(Predicate::WindowMatch, w)) { + if (timestamp == CurrentTime) + 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) +{ + Client* 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) +{ + Client* 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) +{ + Client* 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 (Client* 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(Client * 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 = NULL; // only used when the object is passed to Deleted +} + +} // namespace diff --git a/netinfo.h b/netinfo.h new file mode 100644 index 0000000..94939de --- /dev/null +++ b/netinfo.h @@ -0,0 +1,100 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2009 Lucas Murray +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_NETINFO_H +#define KWIN_NETINFO_H + +#include + +#include +#include + +namespace KWin +{ + +class AbstractClient; +class Client; +class RootInfoFilter; + +/** + * NET WM Protocol handler class + */ +class RootInfo : public NETRootInfo +{ +private: + typedef KWin::Client Client; // Because of NET::Client + +public: + static RootInfo *create(); + static void destroy(); + + void setActiveClient(AbstractClient *client); + +protected: + virtual void changeNumberOfDesktops(int n) override; + virtual void changeCurrentDesktop(int d) override; + virtual void changeActiveWindow(xcb_window_t w, NET::RequestSource src, xcb_timestamp_t timestamp, xcb_window_t active_window) override; + virtual void closeWindow(xcb_window_t w) override; + virtual void moveResize(xcb_window_t w, int x_root, int y_root, unsigned long direction) override; + virtual void moveResizeWindow(xcb_window_t w, int flags, int x, int y, int width, int height) override; + virtual void gotPing(xcb_window_t w, xcb_timestamp_t timestamp) override; + virtual void restackWindow(xcb_window_t w, RequestSource source, xcb_window_t above, int detail, xcb_timestamp_t timestamp) override; + virtual 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 +{ +private: + typedef KWin::Client Client; // Because of NET::Client + +public: + WinInfo(Client* c, xcb_window_t window, + xcb_window_t rwin, NET::Properties properties, NET::Properties2 properties2); + virtual void changeDesktop(int desktop) override; + virtual void changeFullscreenMonitors(NETFullscreenMonitors topology) override; + virtual void changeState(NET::States state, NET::States mask) override; + void disable(); + +private: + Client * m_client; +}; + +} // KWin + +#endif diff --git a/onscreennotification.cpp b/onscreennotification.cpp new file mode 100644 index 0000000..a19b96b --- /dev/null +++ b/onscreennotification.cpp @@ -0,0 +1,244 @@ +/* + * Copyright 2016 Martin Graesslin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#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 + +using namespace KWin; + +class KWin::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::InOutQuad); + } + } +} + +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); + } +} diff --git a/onscreennotification.h b/onscreennotification.h new file mode 100644 index 0000000..92fddd0 --- /dev/null +++ b/onscreennotification.h @@ -0,0 +1,94 @@ +/* + * Copyright 2016 Martin Graesslin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#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..a8cd58f --- /dev/null +++ b/options.cpp @@ -0,0 +1,1148 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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_legacyFullscreenSupport(false) + , m_killPingTimeout(0) + , m_hideUtilityWindowsForInactive(false) + , m_inactiveTabsSkipTaskbar(false) + , m_autogroupSimilarWindows(false) + , m_autogroupInForeground(false) + , m_compositingMode(Options::defaultCompositingMode()) + , m_useCompositing(Options::defaultUseCompositing()) + , m_compositingInitialized(Options::defaultCompositingInitialized()) + , 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) + , animationSpeed(Options::defaultAnimationSpeed()) +{ + m_settings->setDefaults(); + syncFromKcfgc(); +} + +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::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::setLegacyFullscreenSupport(bool legacyFullscreenSupport) +{ + if (m_legacyFullscreenSupport == legacyFullscreenSupport) { + return; + } + m_legacyFullscreenSupport = legacyFullscreenSupport; + emit legacyFullscreenSupportChanged(); +} + +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::setInactiveTabsSkipTaskbar(bool inactiveTabsSkipTaskbar) +{ + if (m_inactiveTabsSkipTaskbar == inactiveTabsSkipTaskbar) { + return; + } + m_inactiveTabsSkipTaskbar = inactiveTabsSkipTaskbar; + emit inactiveTabsSkipTaskbarChanged(); +} + +void Options::setAutogroupSimilarWindows(bool autogroupSimilarWindows) +{ + if (m_autogroupSimilarWindows == autogroupSimilarWindows) { + return; + } + m_autogroupSimilarWindows = autogroupSimilarWindows; + emit autogroupSimilarWindowsChanged(); +} + +void Options::setAutogroupInForeground(bool autogroupInForeground) +{ + if (m_autogroupInForeground == autogroupInForeground) { + return; + } + m_autogroupInForeground = autogroupInForeground; + emit autogroupInForegroundChanged(); +} + +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::setCompositingInitialized(bool compositingInitialized) +{ + if (m_compositingInitialized == compositingInitialized) { + return; + } + m_compositingInitialized = compositingInitialized; + emit compositingInitializedChanged(); +} + +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 cpying is very fast with the nvidia blob + // but due to restrictions in DRI2 *incredibly* slow for all MESA drivers + // see http://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 + setCompositingInitialized(false); + 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", "Switch to Window Tab to the Left/Right")); + CmdAllModKey = (config.readEntry("CommandAllKey", "Alt") == 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", "Start Window Tab Drag"), true)); + setCommandActiveTitlebar3(mouseCommand(config.readEntry("CommandActiveTitlebar3", "Operations menu"), true)); + setCommandInactiveTitlebar1(mouseCommand(config.readEntry("CommandInactiveTitlebar1", "Activate and raise"), true)); + setCommandInactiveTitlebar2(mouseCommand(config.readEntry("CommandInactiveTitlebar2", "Start Window Tab Drag"), 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()); + setLegacyFullscreenSupport(m_settings->legacyFullscreenSupport()); + setFocusStealingPreventionLevel(m_settings->focusStealingPreventionLevel()); + +#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()); + setInactiveTabsSkipTaskbar(m_settings->inactiveTabsSkipTaskbar()); + setAutogroupSimilarWindows(m_settings->autogroupSimilarWindows()); + setAutogroupInForeground(m_settings->autogroupInForeground()); + 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(); + // from now on we've an initial setup and don't have to reload settings on compositing activation + // see Workspace::setupCompositing(), composite.cpp + setCompositingInitialized(true); + + // 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).toAscii(); + 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); + + // TOOD: add setter + animationSpeed = qBound(0, config.readEntry("AnimationSpeed", Options::defaultAnimationSpeed()), 6); + + 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 Alt+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("start window tab drag")) return MouseDragTab; + 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("switch to window tab to the left/right")) return MouseWheelChangeCurrentTab; + if (lowerName == QStringLiteral("nothing")) return MouseWheelNothing; + return MouseWheelChangeCurrentTab; +} + +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; + case MouseWheelChangeCurrentTab: + return delta > 0 ? MousePreviousTab : MouseNextTab; + default: + return MouseNothing; + } +} +#endif + +double Options::animationTimeFactor() const +{ + const double factors[] = { 0, 0.2, 0.5, 1, 2, 4, 20 }; + return factors[ animationSpeed ]; +} + +Options::WindowOperation Options::operationMaxButtonClick(Qt::MouseButtons button) const +{ + return button == Qt::RightButton ? opMaxButtonRightClick : + button == Qt::MidButton ? 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..fe35ae0 --- /dev/null +++ b/options.h @@ -0,0 +1,943 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef KWIN_OPTIONS_H +#define KWIN_OPTIONS_H + +#include "main.h" +#include "placement.h" + +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 +}; + +class Settings; + +class KWIN_EXPORT Options : public QObject +{ + Q_OBJECT + Q_ENUMS(FocusPolicy) + Q_ENUMS(GlSwapStrategy) + Q_ENUMS(MouseCommand) + Q_ENUMS(MouseWheelCommand) + Q_ENUMS(WindowOperation) + + Q_PROPERTY(FocusPolicy focusPolicy READ focusPolicy WRITE setFocusPolicy NOTIFY focusPolicyChanged) + 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) + /** + * support legacy fullscreen windows hack: borderless non-netwm windows with screen geometry + */ + Q_PROPERTY(bool legacyFullscreenSupport READ isLegacyFullscreenSupport WRITE setLegacyFullscreenSupport NOTIFY legacyFullscreenSupportChanged) + 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(bool inactiveTabsSkipTaskbar READ isInactiveTabsSkipTaskbar WRITE setInactiveTabsSkipTaskbar NOTIFY inactiveTabsSkipTaskbarChanged) + Q_PROPERTY(bool autogroupSimilarWindows READ isAutogroupSimilarWindows WRITE setAutogroupSimilarWindows NOTIFY autogroupSimilarWindowsChanged) + Q_PROPERTY(bool autogroupInForeground READ isAutogroupInForeground WRITE setAutogroupInForeground NOTIFY autogroupInForegroundChanged) + Q_PROPERTY(int compositingMode READ compositingMode WRITE setCompositingMode NOTIFY compositingModeChanged) + Q_PROPERTY(bool useCompositing READ isUseCompositing WRITE setUseCompositing NOTIFY useCompositingChanged) + Q_PROPERTY(bool compositingInitialized READ isCompositingInitialized WRITE setCompositingInitialized NOTIFY compositingInitializedChanged) + 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 @link glStrictBinding is set by the OpenGL Scene during initialization. + * If @c false @link 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 = NULL); + ~Options(); + + void updateSettings(); + + /*! + Different focus policies: +
    + +
  • ClickToFocus - Clicking into a window activates it. This is + also the default. + +
  • FocusFollowsMouse - 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. + +
  • FocusUnderMouse - 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. + +
  • FocusStrictlyUnderMouse - 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. + + Note that FocusUnderMouse and FocusStrictlyUnderMouse are not + particulary useful. They are only provided for old-fashined + die-hard UNIX people ;-) + +
+ */ + enum FocusPolicy { ClickToFocus, FocusFollowsMouse, FocusUnderMouse, FocusStrictlyUnderMouse }; + FocusPolicy focusPolicy() const { + return m_focusPolicy; + } + bool isNextFocusPrefersMouse() const { + return m_nextFocusPrefersMouse; + } + + /** + 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; + } + + // 0 - 4 , see Workspace::allowClientActivation() + int focusStealingPreventionLevel() const { + return m_focusStealingPreventionLevel; + } + + /** + * support legacy fullscreen windows hack: borderless non-netwm windows with screen geometry + */ + bool isLegacyFullscreenSupport() const { + return m_legacyFullscreenSupport; + } + + 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, + RemoveTabFromGroupOp, // Remove from group + CloseTabGroupOp, // Close the group + ActivateNextTabOp, // Move left in the group + ActivatePreviousTabOp, // Move right in the group + TabDragOp, + }; + + 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, MousePreviousTab, MouseNextTab, MouseDragTab, + MouseNothing + }; + + enum MouseWheelCommand { + MouseWheelRaiseLower, MouseWheelShadeUnshade, MouseWheelMaximizeRestore, + MouseWheelAboveBelow, MouseWheelPreviousNextDesktop, + MouseWheelChangeOpacity, MouseWheelChangeCurrentTab, + 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; + } + 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; + } + + bool isInactiveTabsSkipTaskbar() const { + return m_inactiveTabsSkipTaskbar; + } + bool isAutogroupSimilarWindows() const { + return m_autogroupSimilarWindows; + } + bool isAutogroupInForeground() const { + return m_autogroupInForeground; + } + + // 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; + bool isCompositingInitialized() const { + return m_compositingInitialized; + } + + // 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 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 setLegacyFullscreenSupport(bool legacyFullscreenSupport); + 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 setInactiveTabsSkipTaskbar(bool inactiveTabsSkipTaskbar); + void setAutogroupSimilarWindows(bool autogroupSimilarWindows); + void setAutogroupInForeground(bool autogroupInForeground); + void setCompositingMode(int compositingMode); + void setUseCompositing(bool useCompositing); + void setCompositingInitialized(bool compositingInitialized); + 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 MouseDragTab; + } + static MouseCommand defaultCommandActiveTitlebar3() { + return MouseOperationsMenu; + } + static MouseCommand defaultCommandInactiveTitlebar1() { + return MouseActivateAndRaise; + } + static MouseCommand defaultCommandInactiveTitlebar2() { + return MouseDragTab; + } + 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 MouseWheelChangeCurrentTab; + } + static MouseWheelCommand defaultCommandAllWheel() { + return MouseWheelNothing; + } + static uint defaultKeyCmdAllModKey() { + return Qt::Key_Alt; + } + static bool defaultAutogroupInForeground() { + return true; + } + static CompositingType defaultCompositingMode() { + return OpenGLCompositing; + } + static bool defaultUseCompositing() { + return true; + } + static bool defaultCompositingInitialized() { + return false; + } + 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 int defaultAnimationSpeed() { + 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 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 legacyFullscreenSupportChanged(); + 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 inactiveTabsSkipTaskbarChanged(); + void autogroupSimilarWindowsChanged(); + void autogroupInForegroundChanged(); + void compositingModeChanged(); + void useCompositingChanged(); + void compositingInitializedChanged(); + 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 configChanged(); + +private: + void setElectricBorders(int borders); + void syncFromKcfgc(); + QScopedPointer m_settings; + 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; + bool m_legacyFullscreenSupport; + int m_killPingTimeout; + bool m_hideUtilityWindowsForInactive; + bool m_inactiveTabsSkipTaskbar; + bool m_autogroupSimilarWindows; + bool m_autogroupInForeground; + + CompositingType m_compositingMode; + bool m_useCompositing; + bool m_compositingInitialized; + 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; + int animationSpeed; // 0 - instant, 5 - very slow + + 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.freedesktop.ScreenSaver.xml b/org.freedesktop.ScreenSaver.xml new file mode 100644 index 0000000..5efd943 --- /dev/null +++ b/org.freedesktop.ScreenSaver.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/org.kde.KWin.xml b/org.kde.KWin.xml new file mode 100644 index 0000000..4794335 --- /dev/null +++ b/org.kde.KWin.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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..0b83a01 --- /dev/null +++ b/org.kde.kwin.ColorCorrect.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + 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/org.kde.kwin.OrientationSensor.xml b/org.kde.kwin.OrientationSensor.xml new file mode 100644 index 0000000..12f245c --- /dev/null +++ b/org.kde.kwin.OrientationSensor.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/orientation_sensor.cpp b/orientation_sensor.cpp new file mode 100644 index 0000000..c78a379 --- /dev/null +++ b/orientation_sensor.cpp @@ -0,0 +1,153 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#include "orientation_sensor.h" +#include + +#include +#include + +#include +#include +#include + +namespace KWin +{ + +OrientationSensor::OrientationSensor(QObject *parent) + : QObject(parent) + , m_sensor(new QOrientationSensor(this)) +{ + connect(m_sensor, &QOrientationSensor::readingChanged, this, + [this] { + auto toOrientation = [] (auto reading) { + switch (reading->orientation()) { + case QOrientationReading::Undefined: + return OrientationSensor::Orientation::Undefined; + case QOrientationReading::TopUp: + return OrientationSensor::Orientation::TopUp; + case QOrientationReading::TopDown: + return OrientationSensor::Orientation::TopDown; + case QOrientationReading::LeftUp: + return OrientationSensor::Orientation::LeftUp; + case QOrientationReading::RightUp: + return OrientationSensor::Orientation::RightUp; + case QOrientationReading::FaceUp: + return OrientationSensor::Orientation::FaceUp; + case QOrientationReading::FaceDown: + return OrientationSensor::Orientation::FaceDown; + default: + Q_UNREACHABLE(); + } + }; + const auto orientation = toOrientation(m_sensor->reading()); + if (m_orientation != orientation) { + m_orientation = orientation; + emit orientationChanged(); + } + } + ); + connect(m_sensor, &QOrientationSensor::activeChanged, this, + [this] { + if (!m_sni) { + return; + } + if (m_sensor->isActive()) { + m_sni->setToolTipTitle(i18n("Automatic screen rotation is enabled")); + } else { + m_sni->setToolTipTitle(i18n("Automatic screen rotation is disabled")); + } + } + ); +} + +OrientationSensor::~OrientationSensor() = default; + +void OrientationSensor::setEnabled(bool enabled) +{ + if (m_enabled == enabled) { + return; + } + m_enabled = enabled; + if (m_enabled) { + loadConfig(); + setupStatusNotifier(); + m_adaptor = new OrientationSensorAdaptor(this); + } else { + delete m_sni; + m_sni = nullptr; + delete m_adaptor; + m_adaptor = nullptr; + } + startStopSensor(); +} + +void OrientationSensor::loadConfig() +{ + if (!m_config) { + return; + } + m_userEnabled = m_config->group("OrientationSensor").readEntry("Enabled", true); +} + +void OrientationSensor::setupStatusNotifier() +{ + if (m_sni) { + return; + } + m_sni = new KStatusNotifierItem(QStringLiteral("kwin-automatic-rotation"), this); + m_sni->setStandardActionsEnabled(false); + m_sni->setCategory(KStatusNotifierItem::Hardware); + m_sni->setStatus(KStatusNotifierItem::Passive); + m_sni->setTitle(i18n("Automatic Screen Rotation")); + // TODO: proper icon with state + m_sni->setIconByName(QStringLiteral("preferences-desktop-display")); + // we start disabled, it gets updated when the sensor becomes active + m_sni->setToolTipTitle(i18n("Automatic screen rotation is disabled")); + connect(m_sni, &KStatusNotifierItem::activateRequested, this, + [this] { + m_userEnabled = !m_userEnabled; + startStopSensor(); + emit userEnabledChanged(m_userEnabled); + } + ); +} + +void OrientationSensor::startStopSensor() +{ + if (m_enabled && m_userEnabled) { + m_sensor->start(); + } else { + m_sensor->stop(); + } +} + +void OrientationSensor::setUserEnabled(bool enabled) +{ + if (m_userEnabled == enabled) { + return; + } + m_userEnabled = enabled; + if (m_config) { + m_config->group("OrientationSensor").writeEntry("Enabled", m_userEnabled); + } + emit userEnabledChanged(m_userEnabled); +} + +} diff --git a/orientation_sensor.h b/orientation_sensor.h new file mode 100644 index 0000000..29b2ef7 --- /dev/null +++ b/orientation_sensor.h @@ -0,0 +1,91 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#pragma once + +#include + +#include + +#include + +class QOrientationSensor; +class OrientationSensorAdaptor; +class KStatusNotifierItem; + +namespace KWin +{ + +class KWIN_EXPORT OrientationSensor : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.kwin.OrientationSensor") + Q_PROPERTY(bool userEnabled READ isUserEnabled WRITE setUserEnabled NOTIFY userEnabledChanged) +public: + explicit OrientationSensor(QObject *parent = nullptr); + ~OrientationSensor(); + + void setEnabled(bool enabled); + + /** + * Just like QOrientationReading::Orientation, + * copied to not leak the QSensors API into internal API. + **/ + enum class Orientation { + Undefined, + TopUp, + TopDown, + LeftUp, + RightUp, + FaceUp, + FaceDown + }; + + Orientation orientation() const { + return m_orientation; + } + + void setConfig(KSharedConfig::Ptr config) { + m_config = config; + } + + bool isUserEnabled() const { + return m_userEnabled; + } + void setUserEnabled(bool enabled); + +Q_SIGNALS: + void orientationChanged(); + void userEnabledChanged(bool); + +private: + void setupStatusNotifier(); + void startStopSensor(); + void loadConfig(); + QOrientationSensor *m_sensor; + bool m_enabled = false; + bool m_userEnabled = true; + Orientation m_orientation = Orientation::Undefined; + KStatusNotifierItem *m_sni = nullptr; + KSharedConfig::Ptr m_config; + OrientationSensorAdaptor *m_adaptor = nullptr; + +}; + +} diff --git a/osd.cpp b/osd.cpp new file mode 100644 index 0000000..601b860 --- /dev/null +++ b/osd.cpp @@ -0,0 +1,82 @@ +/* + * Copyright 2016 Martin Graesslin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +#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..9e45822 --- /dev/null +++ b/osd.h @@ -0,0 +1,45 @@ +/* + * Copyright 2016 Martin Graesslin + * + * 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) version 3 or any later version + * 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 + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#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..69a8a54 --- /dev/null +++ b/outline.cpp @@ -0,0 +1,186 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Arthur Arlt +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +// 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())); + } + } +} + +} // namespace diff --git a/outline.h b/outline.h new file mode 100644 index 0000000..982c97f --- /dev/null +++ b/outline.h @@ -0,0 +1,194 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Arthur Arlt + +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, see . +*********************************************************************/ + +#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(); + + /** + * Set the outline geometry. + * To show the outline use @link 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 @link 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 @link 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 @link 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); + virtual ~CompositedOutlineVisual(); + virtual void show(); + virtual void hide(); +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..d6dd84a --- /dev/null +++ b/outputscreens.cpp @@ -0,0 +1,132 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#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 +{ + const auto enOuts = m_platform->enabledOutputs(); + if (screen >= enOuts.size()) { + return Screens::name(screen); + } + return enOuts.at(screen)->name(); +} + +bool OutputScreens::isInternal(int screen) const +{ + const auto enOuts = m_platform->enabledOutputs(); + if (screen >= enOuts.size()) { + return false; + } + return enOuts.at(screen)->isInternal(); +} + +QRect OutputScreens::geometry(int screen) const +{ + const auto enOuts = m_platform->enabledOutputs(); + if (screen >= enOuts.size()) { + return QRect(); + } + return enOuts.at(screen)->geometry(); +} + +QSize OutputScreens::size(int screen) const +{ + const auto enOuts = m_platform->enabledOutputs(); + if (screen >= enOuts.size()) { + return QSize(); + } + return enOuts.at(screen)->geometry().size(); +} + +qreal OutputScreens::scale(int screen) const +{ + const auto enOuts = m_platform->enabledOutputs(); + if (screen >= enOuts.size()) { + return 1; + } + return enOuts.at(screen)->scale(); +} + +QSizeF OutputScreens::physicalSize(int screen) const +{ + const auto enOuts = m_platform->enabledOutputs(); + if (screen >= enOuts.size()) { + return Screens::physicalSize(screen); + } + return enOuts.at(screen)->physicalSize(); +} + +Qt::ScreenOrientation OutputScreens::orientation(int screen) const +{ + const auto enOuts = m_platform->enabledOutputs(); + if (screen >= enOuts.size()) { + return Qt::PrimaryOrientation; + } + return enOuts.at(screen)->orientation(); +} + +void OutputScreens::updateCount() +{ + setCount(m_platform->enabledOutputs().size()); +} + +int OutputScreens::number(const QPoint &pos) const +{ + int bestScreen = 0; + int minDistance = INT_MAX; + const auto enOuts = m_platform->enabledOutputs(); + for (int i = 0; i < enOuts.size(); ++i) { + const QRect &geo = enOuts.at(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; +} + +} // namespace diff --git a/outputscreens.h b/outputscreens.h new file mode 100644 index 0000000..4d31acb --- /dev/null +++ b/outputscreens.h @@ -0,0 +1,55 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#ifndef KWIN_OUTPUTSCREENS_H +#define KWIN_OUTPUTSCREENS_H + +#include "screens.h" + +namespace KWin +{ + +/** + * @brief Implementation for backends with Outputs + **/ +class KWIN_EXPORT OutputScreens : public Screens +{ + Q_OBJECT +public: + OutputScreens(Platform *platform, QObject *parent = nullptr); + virtual ~OutputScreens(); + + void init() override; + QString name(int screen) const override; + bool isInternal(int screen) const; + QSizeF physicalSize(int screen) const; + QRect geometry(int screen) const override; + QSize size(int screen) const override; + qreal scale(int screen) const override; + Qt::ScreenOrientation orientation(int screen) const; + void updateCount() override; + int number(const QPoint &pos) const override; + +protected: + Platform *m_platform; +}; + +} + +#endif // KWIN_OUTPUTSCREENS_H diff --git a/overlaywindow.cpp b/overlaywindow.cpp new file mode 100644 index 0000000..ac69751 --- /dev/null +++ b/overlaywindow.cpp @@ -0,0 +1,32 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Arthur Arlt + +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, see . +*********************************************************************/ + +#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..0afff21 --- /dev/null +++ b/overlaywindow.h @@ -0,0 +1,52 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Arthur Arlt + +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, see . +*********************************************************************/ + +#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/packageplugins/CMakeLists.txt b/packageplugins/CMakeLists.txt new file mode 100644 index 0000000..92d6d1b --- /dev/null +++ b/packageplugins/CMakeLists.txt @@ -0,0 +1,4 @@ +add_subdirectory(aurorae) +add_subdirectory(decoration) +add_subdirectory(scripts) +add_subdirectory(windowswitcher) diff --git a/packageplugins/aurorae/CMakeLists.txt b/packageplugins/aurorae/CMakeLists.txt new file mode 100644 index 0000000..6b1ef2b --- /dev/null +++ b/packageplugins/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/packageplugins/aurorae/aurorae.cpp b/packageplugins/aurorae/aurorae.cpp new file mode 100644 index 0000000..8628f79 --- /dev/null +++ b/packageplugins/aurorae/aurorae.cpp @@ -0,0 +1,85 @@ +/****************************************************************************** +* Copyright 2017 by Demitrius Belai * +* * +* 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; see the file COPYING.LIB. If not, write to * +* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * +* Boston, MA 02110-1301, USA. * +*******************************************************************************/ + +#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/packageplugins/aurorae/aurorae.h b/packageplugins/aurorae/aurorae.h new file mode 100644 index 0000000..c3005aa --- /dev/null +++ b/packageplugins/aurorae/aurorae.h @@ -0,0 +1,33 @@ +/****************************************************************************** +* Copyright 2017 by Demitrius Belai * +* * +* 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; see the file COPYING.LIB. If not, write to * +* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * +* Boston, MA 02110-1301, USA. * +*******************************************************************************/ + +#ifndef AURORAEPACKAGE_H +#define AURORAEPACKAGE_H + +#include + +class AuroraePackage : public KPackage::PackageStructure +{ +public: + AuroraePackage(QObject*, const QVariantList &) {} + void initPackage(KPackage::Package *package) Q_DECL_OVERRIDE; + void pathChanged(KPackage::Package *package) Q_DECL_OVERRIDE; +}; + +#endif diff --git a/packageplugins/aurorae/kwin-packagestructure-aurorae.desktop b/packageplugins/aurorae/kwin-packagestructure-aurorae.desktop new file mode 100644 index 0000000..4b7a57d --- /dev/null +++ b/packageplugins/aurorae/kwin-packagestructure-aurorae.desktop @@ -0,0 +1,44 @@ +[Desktop Entry] +Name=KWin Aurorae +Name[ca]=Aurorae del KWin +Name[ca@valencia]=Aurorae del 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[eu]=KWin Aurorae +Name[fi]=KWin Aurorae +Name[fr]=Module Aurorae de KWin +Name[gl]=Aurorae de KWin +Name[hu]=KWin Aurorae +Name[id]=KWin Aurorae +Name[it]=Aurorae di Kwin +Name[ko]=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[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/packageplugins/decoration/CMakeLists.txt b/packageplugins/decoration/CMakeLists.txt new file mode 100644 index 0000000..bfb6065 --- /dev/null +++ b/packageplugins/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/packageplugins/decoration/decoration.cpp b/packageplugins/decoration/decoration.cpp new file mode 100644 index 0000000..a8dc37d --- /dev/null +++ b/packageplugins/decoration/decoration.cpp @@ -0,0 +1,62 @@ +/****************************************************************************** +* Copyright 2017 by Demitrius Belai * +* * +* 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; see the file COPYING.LIB. If not, write to * +* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * +* Boston, MA 02110-1301, USA. * +*******************************************************************************/ + +#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/packageplugins/decoration/decoration.h b/packageplugins/decoration/decoration.h new file mode 100644 index 0000000..cf3897e --- /dev/null +++ b/packageplugins/decoration/decoration.h @@ -0,0 +1,33 @@ +/****************************************************************************** +* Copyright 2017 by Demitrius Belai * +* * +* 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; see the file COPYING.LIB. If not, write to * +* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * +* Boston, MA 02110-1301, USA. * +*******************************************************************************/ + +#ifndef DECORATIONPACKAGE_H +#define DECORATIONPACKAGE_H + +#include + +class DecorationPackage : public KPackage::PackageStructure +{ +public: + DecorationPackage(QObject*, const QVariantList &) {} + void initPackage(KPackage::Package *package) Q_DECL_OVERRIDE; + void pathChanged(KPackage::Package *package) Q_DECL_OVERRIDE; +}; + +#endif diff --git a/packageplugins/decoration/kwin-packagestructure-decoration.desktop b/packageplugins/decoration/kwin-packagestructure-decoration.desktop new file mode 100644 index 0000000..519611c --- /dev/null +++ b/packageplugins/decoration/kwin-packagestructure-decoration.desktop @@ -0,0 +1,47 @@ +[Desktop Entry] +Name=KWin Decoration +Name[ca]=Decoració del KWin +Name[ca@valencia]=Decoració del 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[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]=KWin Decoration +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[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/packageplugins/scripts/CMakeLists.txt b/packageplugins/scripts/CMakeLists.txt new file mode 100644 index 0000000..f0a5636 --- /dev/null +++ b/packageplugins/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/packageplugins/scripts/kwin-packagestructure-scripts.desktop b/packageplugins/scripts/kwin-packagestructure-scripts.desktop new file mode 100644 index 0000000..08b9927 --- /dev/null +++ b/packageplugins/scripts/kwin-packagestructure-scripts.desktop @@ -0,0 +1,47 @@ +[Desktop Entry] +Name=KWin Script +Name[ca]=Script del KWin +Name[ca@valencia]=Script del 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[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]=KWin Script +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[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/packageplugins/scripts/scripts.cpp b/packageplugins/scripts/scripts.cpp new file mode 100644 index 0000000..4a41514 --- /dev/null +++ b/packageplugins/scripts/scripts.cpp @@ -0,0 +1,62 @@ +/****************************************************************************** +* Copyright 2017 by Marco Martin * +* * +* 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; see the file COPYING.LIB. If not, write to * +* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * +* Boston, MA 02110-1301, USA. * +*******************************************************************************/ + +#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/packageplugins/scripts/scripts.h b/packageplugins/scripts/scripts.h new file mode 100644 index 0000000..b5b7035 --- /dev/null +++ b/packageplugins/scripts/scripts.h @@ -0,0 +1,33 @@ +/****************************************************************************** +* Copyright 2017 by Marco Martin * +* * +* 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; see the file COPYING.LIB. If not, write to * +* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * +* Boston, MA 02110-1301, USA. * +*******************************************************************************/ + +#ifndef SCRIPTSPACKAGE_H +#define SCRIPTSPACKAGE_H + +#include + +class ScriptsPackage : public KPackage::PackageStructure +{ +public: + ScriptsPackage(QObject*, const QVariantList &) {} + void initPackage(KPackage::Package *package) Q_DECL_OVERRIDE; + void pathChanged(KPackage::Package *package) Q_DECL_OVERRIDE; +}; + +#endif diff --git a/packageplugins/windowswitcher/CMakeLists.txt b/packageplugins/windowswitcher/CMakeLists.txt new file mode 100644 index 0000000..d0496d4 --- /dev/null +++ b/packageplugins/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/packageplugins/windowswitcher/kwin-packagestructure-windowswitcher.desktop b/packageplugins/windowswitcher/kwin-packagestructure-windowswitcher.desktop new file mode 100644 index 0000000..c220697 --- /dev/null +++ b/packageplugins/windowswitcher/kwin-packagestructure-windowswitcher.desktop @@ -0,0 +1,47 @@ +[Desktop Entry] +Name=KWin Window Switcher +Name[ca]=Commutador de finestres del KWin +Name[ca@valencia]=Commutador de finestres del 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[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]=KWin Window Switcher +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[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/packageplugins/windowswitcher/windowswitcher.cpp b/packageplugins/windowswitcher/windowswitcher.cpp new file mode 100644 index 0000000..a32d953 --- /dev/null +++ b/packageplugins/windowswitcher/windowswitcher.cpp @@ -0,0 +1,62 @@ +/****************************************************************************** +* Copyright 2017 by Marco Martin * +* * +* 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; see the file COPYING.LIB. If not, write to * +* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * +* Boston, MA 02110-1301, USA. * +*******************************************************************************/ + +#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/packageplugins/windowswitcher/windowswitcher.h b/packageplugins/windowswitcher/windowswitcher.h new file mode 100644 index 0000000..141fcaf --- /dev/null +++ b/packageplugins/windowswitcher/windowswitcher.h @@ -0,0 +1,33 @@ +/****************************************************************************** +* Copyright 2017 by Marco Martin * +* * +* 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; see the file COPYING.LIB. If not, write to * +* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * +* Boston, MA 02110-1301, USA. * +*******************************************************************************/ + +#ifndef WINDOWSWITCHER_H +#define WINDOWSWITCHER_H + +#include + +class SwitcherPackage : public KPackage::PackageStructure +{ +public: + SwitcherPackage(QObject*, const QVariantList &) {} + void initPackage(KPackage::Package *package) Q_DECL_OVERRIDE; + void pathChanged(KPackage::Package *package) Q_DECL_OVERRIDE; +}; + +#endif diff --git a/placement.cpp b/placement.cpp new file mode 100644 index 0000000..cac3fe9 --- /dev/null +++ b/placement.cpp @@ -0,0 +1,965 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 1997 to 2002 Cristian Tibirna +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ + +#include "placement.h" + +#include +#include + +#include + +#ifndef KCMRULES +#include "workspace.h" +#include "client.h" +#include "cursor.h" +#include "options.h" +#include "rules.h" +#include "screens.h" +#endif + +namespace KWin +{ + +#ifndef KCMRULES + +KWIN_SINGLETON_FACTORY(Placement) + +Placement::Placement(QObject*) +{ + reinitCascading(0); +} + +Placement::~Placement() +{ + s_self = NULL; +} + +/*! + Places the client \a c according to the workspace's layout policy + */ +void Placement::place(AbstractClient* c, 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()) + placeOnScreenDisplay(c, area); + else if (c->isTransient() && c->hasTransientPlacementHint()) + placeTransient(c); + else + place(c, area, options->placement()); +} + +void Placement::place(AbstractClient* c, 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->geometry()); + QPoint corner = geo.topLeft(); + const QPoint cp = c->clientPos(); + const QSize cs = geo.size() - c->clientSize(); + Client::Position titlePos = c->titlebarPosition(); + + const QRect fullRect = workspace()->clientArea(FullArea, c); + if (!(c->maximizeMode() & MaximizeHorizontal)) { + if (titlePos != Client::PositionRight && geo.right() == fullRect.right()) + corner.rx() += cs.width() - cp.x(); + if (titlePos != Client::PositionLeft && geo.x() == fullRect.x()) + corner.rx() -= cp.x(); + } + if (!(c->maximizeMode() & MaximizeVertical)) { + if (titlePos != Client::PositionBottom && geo.bottom() == fullRect.bottom()) + corner.ry() += cs.height() - cp.y(); + if (titlePos != Client::PositionTop && geo.y() == fullRect.y()) + corner.ry() -= cp.y(); + } + 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*/) +{ + const int step = 24; + static int px = step; + static int py = 2 * step; + int tx, ty; + + const QRect maxRect = checkArea(c, area); + + if (px < maxRect.x()) + px = maxRect.x(); + if (py < maxRect.y()) + py = maxRect.y(); + + px += step; + py += 2 * step; + + if (px > maxRect.width() / 2) + px = maxRect.x() + step; + if (py > maxRect.height() / 2) + py = maxRect.y() + step; + tx = px; + ty = py; + if (tx + c->width() > maxRect.right()) { + tx = maxRect.right() - c->width(); + if (tx < 0) + tx = 0; + px = maxRect.x(); + } + if (ty + c->height() > maxRect.bottom()) { + ty = maxRect.bottom() - c->height(); + if (ty < 0) + ty = 0; + py = maxRect.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->isCurrentTab()) + 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*/) +{ + /* + * SmartPlacement by Cristian Tibirna (tibirna@kde.org) + * adapted for kwm (16-19jan98) and for kwin (16Nov1999) using (with + * permission) ideas from fvwm, authored by + * Anthony Martin (amartin@engr.csulb.edu). + * Xinerama supported added by Balaji Ramani (balaji@yablibli.com) + * with ideas from xfce. + */ + + const 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 + const QRect maxRect = checkArea(c, area); + int x = maxRect.left(), y = maxRect.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 > maxRect.bottom() && ch < maxRect.height()) + overlap = h_wrong; // this throws the algorithm to an exit + else if (x + cw > maxRect.right()) + overlap = w_wrong; + else { + overlap = none; //initialize + + cxl = x; cxr = x + cw; + cyt = y; cyb = y + ch; + ToplevelList::ConstIterator l; + for (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 Client::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 = maxRect.right(); + if (possible - cw > x) possible -= cw; + + // compare to the position of each client on the same desk + ToplevelList::ConstIterator l; + for (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 = maxRect.left(); + possible = maxRect.bottom(); + + if (possible - ch > y) possible -= ch; + + //test the position of each window on the desk + ToplevelList::ConstIterator l; + for (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 < maxRect.bottom())); + + if (ch >= maxRect.height()) + y_optimal = maxRect.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->geometry().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, QRect& area, Policy nextPlacement) +{ + /* 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); + + // get the maximum allowed windows space and desk's origin + QRect maxRect = checkArea(c, area); + + // 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 = maxRect.left(); + const int Y = maxRect.top(); + const int H = maxRect.height(); + const int W = maxRect.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*/) +{ + + // get the maximum allowed windows space and desk's origin + const QRect maxRect = checkArea(c, area); + + const int xp = maxRect.left() + (maxRect.width() - c->width()) / 2; + const int yp = maxRect.top() + (maxRect.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*/) +{ + // get the maximum allowed windows space and desk's origin + c->move(checkArea(c, area).topLeft()); +} + +void Placement::placeUtility(AbstractClient* c, 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, QRect& area) +{ + // place at lower 1/3 of the screen + const int x = area.left() + (area.width() - c->width()) / 2; + const int y = area.top() + 2 * (area.height() - c->height()) / 3; + + c->move(QPoint(x, y)); +} + +void Placement::placeTransient(AbstractClient *c) +{ + const QPoint target = c->transientFor()->pos() + c->transientFor()->clientPos() + c->transientPlacementHint(); + c->move(target); + const QRect screen = screens()->geometry(c->transientFor()->screen()); + // TODO: work around Qt's transient placement of sub-menus, see https://bugreports.qt.io/browse/QTBUG-51640 +#define CHECK \ + if (screen.contains(c->geometry())) { \ + return; \ + } + CHECK + if (screen.x() + screen.width() < c->x() + c->width()) { + // overlaps on right + c->move(c->x() - c->width(), c->y()); + CHECK + } + if (screen.y() + screen.height() < c->y() + c->height()) { + // overlaps on bottom + c->move(c->x(), c->y() - c->height()); + CHECK + } + if (screen.y() > c->y()) { + // top is not on screen + c->move(c->x(), screen.y()); + CHECK + } + if (screen.x() > c->x()) { + // left is not on screen + c->move(screen.x(), c->y()); + CHECK + } +#undef CHECK + // so far the sanitizing didn't help, let's move back to orig target position and use keepInArea + c->move(target); + c->keepInArea(screen); +} + +void Placement::placeDialog(AbstractClient* c, QRect& area, Policy nextPlacement) +{ + placeOnMainWindow(c, area, nextPlacement); +} + +void Placement::placeUnderMouse(AbstractClient* c, QRect& area, Policy /*next*/) +{ + area = checkArea(c, area); + QRect geom = c->geometry(); + geom.moveCenter(Cursor::pos()); + c->move(geom.topLeft()); + c->keepInArea(area); // make sure it's kept inside workarea +} + +void Placement::placeOnMainWindow(AbstractClient* c, QRect& area, Policy nextPlacement) +{ + if (nextPlacement == Unknown) + nextPlacement = Centered; + if (nextPlacement == Maximizing) // maximize if needed + placeMaximizing(c, area, NoPlacement); + area = checkArea(c, area); + 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 == NULL) + 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 == NULL) { + // '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->geometry(); + geom.moveCenter(place_on->geometry().center()); + c->move(geom.topLeft()); + // get area again, because the mainwindow may be on different xinerama screen + area = checkArea(c, QRect()); + c->keepInArea(area); // make sure it's kept inside workarea +} + +void Placement::placeMaximizing(AbstractClient* c, QRect& area, Policy nextPlacement) +{ + 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->setGeometry(area); + } + } else { + c->resizeWithChecks(c->maxSize().boundedTo(area.size())); + place(c, area, nextPlacement); + } +} + +void Placement::cascadeDesktop() +{ +// TODO XINERAMA this probably is not right for xinerama + Workspace *ws = Workspace::self(); + const int desktop = VirtualDesktopManager::self()->current(); + reinitCascading(desktop); + // TODO: make area const once placeFoo methods are fixed to take a const QRect& + QRect area = ws->clientArea(PlacementArea, QPoint(0, 0), desktop); + foreach (Toplevel *toplevel, ws->stackingOrder()) { + auto client = qobject_cast(toplevel); + if (!client || + (!client->isOnCurrentDesktop()) || + (client->isMinimized()) || + (client->isOnAllDesktops()) || + (!client->isMovable())) + continue; + placeCascaded(client, area); + } +} + +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; + placeSmart(client, QRect()); + } +} + +QRect Placement::checkArea(const AbstractClient* c, const QRect& area) +{ + if (area.isNull()) + return workspace()->clientArea(PlacementArea, c->geometry().center(), c->desktop()); + return area; +} + +#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" + }; + assert(policy < int(sizeof(policies) / sizeof(policies[ 0 ]))); + return policies[ policy ]; +} + + +#ifndef KCMRULES + +// ******************** +// Workspace +// ******************** + +void AbstractClient::packTo(int left, int top) +{ + workspace()->updateFocusMousePosition(Cursor::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->geometry().left(), true), + active_client->y()); +} + +void Workspace::slotWindowPackRight() +{ + if (active_client && active_client->isMovable()) + active_client->packTo(packPositionRight(active_client, active_client->geometry().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->geometry().top(), true)); +} + +void Workspace::slotWindowPackDown() +{ + if (active_client && active_client->isMovable()) + active_client->packTo(active_client->x(), + packPositionDown(active_client, active_client->geometry().bottom(), true) - active_client->height() + 1); +} + +void Workspace::slotWindowGrowHorizontal() +{ + if (active_client) + active_client->growHorizontal(); +} + +void AbstractClient::growHorizontal() +{ + if (!isResizable() || isShade()) + return; + QRect geom = geometry(); + geom.setRight(workspace()->packPositionRight(this, geom.right(), true)); + QSize adjsize = adjustedSize(geom.size(), SizemodeFixedW); + if (geometry().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, geometry().center().y()), desktop()).right() >= newright) + geom.setRight(newright); + } + geom.setSize(adjustedSize(geom.size(), SizemodeFixedW)); + geom.setSize(adjustedSize(geom.size(), SizemodeFixedH)); + workspace()->updateFocusMousePosition(Cursor::pos()); // may cause leave event; + setGeometry(geom); +} + +void Workspace::slotWindowShrinkHorizontal() +{ + if (active_client) + active_client->shrinkHorizontal(); +} + +void AbstractClient::shrinkHorizontal() +{ + if (!isResizable() || isShade()) + return; + QRect geom = geometry(); + geom.setRight(workspace()->packPositionLeft(this, geom.right(), false)); + if (geom.width() <= 1) + return; + geom.setSize(adjustedSize(geom.size(), SizemodeFixedW)); + if (geom.width() > 20) { + workspace()->updateFocusMousePosition(Cursor::pos()); // may cause leave event; + setGeometry(geom); + } +} + +void Workspace::slotWindowGrowVertical() +{ + if (active_client) + active_client->growVertical(); +} + +void AbstractClient::growVertical() +{ + if (!isResizable() || isShade()) + return; + QRect geom = geometry(); + geom.setBottom(workspace()->packPositionDown(this, geom.bottom(), true)); + QSize adjsize = adjustedSize(geom.size(), SizemodeFixedH); + if (geometry().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(geometry().center().x(), (y() + newbottom) / 2), desktop()).bottom() >= newbottom) + geom.setBottom(newbottom); + } + geom.setSize(adjustedSize(geom.size(), SizemodeFixedH)); + workspace()->updateFocusMousePosition(Cursor::pos()); // may cause leave event; + setGeometry(geom); +} + + +void Workspace::slotWindowShrinkVertical() +{ + if (active_client) + active_client->shrinkVertical(); +} + +void AbstractClient::shrinkVertical() +{ + if (!isResizable() || isShade()) + return; + QRect geom = geometry(); + geom.setBottom(workspace()->packPositionUp(this, geom.bottom(), false)); + if (geom.height() <= 1) + return; + geom.setSize(adjustedSize(geom.size(), SizemodeFixedH)); + if (geom.height() > 20) { + workspace()->updateFocusMousePosition(Cursor::pos()); // may cause leave event; + setGeometry(geom); + } +} + +void Workspace::quickTileWindow(QuickTileMode mode) +{ + if (!active_client) { + return; + } + + active_client->setQuickTileMode(mode, true); +} + +int Workspace::packPositionLeft(const AbstractClient* cl, int oldx, bool left_edge) const +{ + int newx = clientArea(MaximizeArea, cl).left(); + if (oldx <= newx) // try another Xinerama screen + newx = clientArea(MaximizeArea, + QPoint(cl->geometry().left() - 1, cl->geometry().center().y()), cl->desktop()).left(); + if (cl->titlebarPosition() != Client::PositionLeft) { + QRect geo = cl->geometry(); + int rgt = newx - cl->clientPos().x(); + geo.moveRight(rgt); + if (screens()->intersecting(geo) < 2) + newx = rgt; + } + if (oldx <= newx) + return oldx; + const int desktop = cl->desktop() == 0 || cl->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : cl->desktop(); + for (auto it = m_allClients.constBegin(), end = m_allClients.constEnd(); it != end; ++it) { + if (isIrrelevant(*it, cl, desktop)) + continue; + int x = left_edge ? (*it)->geometry().right() + 1 : (*it)->geometry().left() - 1; + if (x > newx && x < oldx + && !(cl->geometry().top() > (*it)->geometry().bottom() // they overlap in Y direction + || cl->geometry().bottom() < (*it)->geometry().top())) + newx = x; + } + return newx; +} + +int Workspace::packPositionRight(const AbstractClient* cl, int oldx, bool right_edge) const +{ + int newx = clientArea(MaximizeArea, cl).right(); + if (oldx >= newx) // try another Xinerama screen + newx = clientArea(MaximizeArea, + QPoint(cl->geometry().right() + 1, cl->geometry().center().y()), cl->desktop()).right(); + if (cl->titlebarPosition() != Client::PositionRight) { + QRect geo = cl->geometry(); + int rgt = newx + cl->width() - (cl->clientSize().width() + cl->clientPos().x()); + geo.moveRight(rgt); + if (screens()->intersecting(geo) < 2) + newx = rgt; + } + if (oldx >= newx) + return oldx; + const int desktop = cl->desktop() == 0 || cl->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : cl->desktop(); + for (auto it = m_allClients.constBegin(), end = m_allClients.constEnd(); it != end; ++it) { + if (isIrrelevant(*it, cl, desktop)) + continue; + int x = right_edge ? (*it)->geometry().left() - 1 : (*it)->geometry().right() + 1; + if (x < newx && x > oldx + && !(cl->geometry().top() > (*it)->geometry().bottom() + || cl->geometry().bottom() < (*it)->geometry().top())) + newx = x; + } + return newx; +} + +int Workspace::packPositionUp(const AbstractClient* cl, int oldy, bool top_edge) const +{ + int newy = clientArea(MaximizeArea, cl).top(); + if (oldy <= newy) // try another Xinerama screen + newy = clientArea(MaximizeArea, + QPoint(cl->geometry().center().x(), cl->geometry().top() - 1), cl->desktop()).top(); + if (cl->titlebarPosition() != Client::PositionTop) { + QRect geo = cl->geometry(); + int top = newy - cl->clientPos().y(); + geo.moveTop(top); + if (screens()->intersecting(geo) < 2) + newy = top; + } + if (oldy <= newy) + return oldy; + const int desktop = cl->desktop() == 0 || cl->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : cl->desktop(); + for (auto it = m_allClients.constBegin(), end = m_allClients.constEnd(); it != end; ++it) { + if (isIrrelevant(*it, cl, desktop)) + continue; + int y = top_edge ? (*it)->geometry().bottom() + 1 : (*it)->geometry().top() - 1; + if (y > newy && y < oldy + && !(cl->geometry().left() > (*it)->geometry().right() // they overlap in X direction + || cl->geometry().right() < (*it)->geometry().left())) + newy = y; + } + return newy; +} + +int Workspace::packPositionDown(const AbstractClient* cl, int oldy, bool bottom_edge) const +{ + int newy = clientArea(MaximizeArea, cl).bottom(); + if (oldy >= newy) // try another Xinerama screen + newy = clientArea(MaximizeArea, + QPoint(cl->geometry().center().x(), cl->geometry().bottom() + 1), cl->desktop()).bottom(); + if (cl->titlebarPosition() != Client::PositionBottom) { + QRect geo = cl->geometry(); + int btm = newy + cl->height() - (cl->clientSize().height() + cl->clientPos().y()); + geo.moveBottom(btm); + if (screens()->intersecting(geo) < 2) + newy = btm; + } + if (oldy >= newy) + return oldy; + const int desktop = cl->desktop() == 0 || cl->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : cl->desktop(); + for (auto it = m_allClients.constBegin(), end = m_allClients.constEnd(); it != end; ++it) { + if (isIrrelevant(*it, cl, desktop)) + continue; + int y = bottom_edge ? (*it)->geometry().top() - 1 : (*it)->geometry().bottom() + 1; + if (y < newy && y > oldy + && !(cl->geometry().left() > (*it)->geometry().right() + || cl->geometry().right() < (*it)->geometry().left())) + newy = y; + } + return newy; +} + +#endif + +} // namespace diff --git a/placement.h b/placement.h new file mode 100644 index 0000000..01b76c8 --- /dev/null +++ b/placement.h @@ -0,0 +1,112 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 1997 to 2002 Cristian Tibirna +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ + +#ifndef KWIN_PLACEMENT_H +#define KWIN_PLACEMENT_H +// KWin +#include +// Qt +#include +#include +#include + +class QObject; + +namespace KWin +{ + +class AbstractClient; +class Client; + +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, QRect& area); + + void placeAtRandom(AbstractClient* c, const QRect& area, Policy next = Unknown); + void placeCascaded(AbstractClient* c, QRect& area, Policy next = Unknown); + void placeSmart(AbstractClient* c, const QRect& area, Policy next = Unknown); + void placeMaximizing(AbstractClient* c, 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, QRect& area, Policy next = Unknown); + void placeUtility(AbstractClient* c, QRect& area, Policy next = Unknown); + void placeOnScreenDisplay(AbstractClient* c, 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, QRect& area, Policy policy, Policy nextPlacement = Unknown); + void placeUnderMouse(AbstractClient* c, QRect& area, Policy next = Unknown); + void placeOnMainWindow(AbstractClient* c, QRect& area, Policy next = Unknown); + void placeTransient(AbstractClient *c); + QRect checkArea(const AbstractClient*c, const QRect& area); + + //CT needed for cascading+ + struct DesktopCascadingInfo { + QPoint pos; + int col; + int row; + }; + + QList cci; + + KWIN_SINGLETON(Placement) +}; + +} // namespace + +#endif diff --git a/platform.cpp b/platform.cpp new file mode 100644 index 0000000..88168ea --- /dev/null +++ b/platform.cpp @@ -0,0 +1,492 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "platform.h" +#include +#include "composite.h" +#include "cursor.h" +#include "effects.h" +#include "input.h" +#include +#include "overlaywindow.h" +#include "outline.h" +#include "pointer_input.h" +#include "scene.h" +#include "screenedge.h" +#include "wayland_server.h" +#include "colorcorrection/manager.h" + +#include + +namespace KWin +{ + +Platform::Platform(QObject *parent) + : QObject(parent) + , m_eglDisplay(EGL_NO_DISPLAY) +{ + setSoftWareCursor(false); + m_colorCorrect = new ColorCorrect::Manager(this); +} + +Platform::~Platform() +{ + if (m_eglDisplay != EGL_NO_DISPLAY) { + eglTerminate(m_eglDisplay); + } +} + +QImage Platform::softwareCursor() const +{ + return input()->pointer()->cursorImage(); +} + +QPoint Platform::softwareCursorHotspot() const +{ + return input()->pointer()->cursorHotSpot(); +} + +PlatformCursorImage Platform::cursorImage() const +{ + return PlatformCursorImage(softwareCursor(), softwareCursorHotspot()); +} + +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; +} + +Edge *Platform::createScreenEdge(ScreenEdges *edges) +{ + return new Edge(edges); +} + +void Platform::createPlatformCursor(QObject *parent) +{ + new InputRedirectionCursor(parent); +} + +void Platform::configurationChangeRequested(KWayland::Server::OutputConfigurationInterface *config) +{ + Q_UNUSED(config) + qCWarning(KWIN_CORE) << "This backend does not support configuration changes."; + + // KCoreAddons needs kwayland's 2b3f9509ac1 to not crash + if (KCoreAddons::version() >= QT_VERSION_CHECK(5, 39, 0)) { + config->setFailed(); + } +} + +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(Cursor::self(), &Cursor::posChanged, this, &Platform::triggerCursorRepaint); + connect(this, &Platform::cursorChanged, this, &Platform::triggerCursorRepaint); + } else { + disconnect(Cursor::self(), &Cursor::posChanged, this, &Platform::triggerCursorRepaint); + disconnect(this, &Platform::cursorChanged, this, &Platform::triggerCursorRepaint); + } +} + +void Platform::triggerCursorRepaint() +{ + if (!Compositor::self()) { + return; + } + Compositor::self()->addRepaint(m_cursor.lastRenderedGeometry); + Compositor::self()->addRepaint(QRect(Cursor::pos() - softwareCursorHotspot(), softwareCursor().size())); +} + +void Platform::markCursorAsRendered() +{ + if (m_softWareCursor) { + m_cursor.lastRenderedGeometry = QRect(Cursor::pos() - softwareCursorHotspot(), softwareCursor().size()); + } + if (input()->pointer()) { + input()->pointer()->markCursorAsRendered(); + } +} + +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) +{ + if (!input()) { + return; + } + input()->processPointerAxis(InputRedirection::PointerAxisHorizontal, delta, time); +} + +void Platform::pointerAxisVertical(qreal delta, quint32 time) +{ + if (!input()) { + return; + } + input()->processPointerAxis(InputRedirection::PointerAxisVertical, delta, 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::supportsQpaContext() const +{ + if (Compositor *c = Compositor::self()) { + return c->scene()->openGLPlatformInterfaceExtensions().contains(QByteArrayLiteral("EGL_KHR_surfaceless_context")); + } + 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; +} + +void Platform::updateXTime() +{ +} + +OutlineVisual *Platform::createOutline(Outline *outline) +{ + if (Compositor::compositing()) { + return new CompositedOutlineVisual(outline); + } + return nullptr; +} + +Decoration::Renderer *Platform::createDecorationRenderer(Decoration::DecoratedClientImpl *client) +{ + if (Compositor::self()->hasScene()) { + 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()); +} + +} diff --git a/platform.h b/platform.h new file mode 100644 index 0000000..a827eec --- /dev/null +++ b/platform.h @@ -0,0 +1,540 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_PLATFORM_H +#define KWIN_PLATFORM_H +#include +#include +#include +#include "fixqopengl.h" + +#include +#include +#include + +#include + +class QAction; + +namespace KWayland { + namespace Server { + class OutputConfigurationInterface; + } +} + +namespace KWin +{ +namespace ColorCorrect { +class Manager; +} + +class AbstractOutput; +class Edge; +class Compositor; +class OverlayWindow; +class OpenGLBackend; +class Outline; +class OutlineVisual; +class QPainterBackend; +class Scene; +class Screens; +class ScreenEdges; +class Toplevel; +class WaylandCursorTheme; + +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: + virtual ~Platform(); + + virtual void init() = 0; + virtual Screens *createScreens(QObject *parent = nullptr); + virtual OpenGLBackend *createOpenGLBackend(); + virtual QPainterBackend *createQPainterBackend(); + /** + * 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. + **/ + virtual bool supportsQpaContext() 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; + } + /** + * 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. + */ + virtual void configurationChangeRequested(KWayland::Server::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 @link{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 + * @link{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 @link{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; + } + QImage softwareCursor() const; + QPoint softwareCursorHotspot() const; + void markCursorAsRendered(); + + /** + * 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 handlesOutputs() const { + return m_handlesOutputs; + } + 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(); + + /** + * Allows a platform to update the X11 timestamp. + * Mostly for the X11 standalone platform to interact with QX11Info. + * + * Default implementation does nothing. This means code relying on the X timestamp being up to date, + * might not be working. E.g. synced X11 window resizing + **/ + virtual 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(); + } + + /* + * 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; + +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); + void pointerAxisVertical(qreal delta, quint32 time); + 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); + +Q_SIGNALS: + void screensQueried(); + void initFailed(); + void cursorChanged(); + 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 handleOutputs() { + m_handlesOutputs = true; + } + 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; + } + + /** + * 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_handlesOutputs = false; + 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; + EGLSurface m_surface = EGL_NO_SURFACE; + int m_hideCursorCounter = 0; + ColorCorrect::Manager *m_colorCorrect = nullptr; + bool m_supportsGammaControl = false; +}; + +} + +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..a8da72a --- /dev/null +++ b/platformsupport/scenes/opengl/CMakeLists.txt @@ -0,0 +1,23 @@ +set(SCENE_OPENGL_BACKEND_SRCS + abstract_egl_backend.cpp + backend.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 KF5::WaylandServer) diff --git a/platformsupport/scenes/opengl/abstract_egl_backend.cpp b/platformsupport/scenes/opengl/abstract_egl_backend.cpp new file mode 100644 index 0000000..6af4f72 --- /dev/null +++ b/platformsupport/scenes/opengl/abstract_egl_backend.cpp @@ -0,0 +1,544 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "abstract_egl_backend.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 +#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; + +#ifndef EGL_WAYLAND_BUFFER_WL +#define EGL_WAYLAND_BUFFER_WL 0x31D5 +#endif +#ifndef EGL_WAYLAND_PLANE_WL +#define EGL_WAYLAND_PLANE_WL 0x31D6 +#endif +#ifndef EGL_WAYLAND_Y_INVERTED_WL +#define EGL_WAYLAND_Y_INVERTED_WL 0x31DB +#endif + +AbstractEglBackend::AbstractEglBackend() + : QObject(nullptr) + , OpenGLBackend() +{ + connect(Compositor::self(), &Compositor::aboutToDestroy, this, &AbstractEglBackend::unbindWaylandDisplay); +} + +AbstractEglBackend::~AbstractEglBackend() = default; + +void AbstractEglBackend::unbindWaylandDisplay() +{ + if (eglUnbindWaylandDisplayWL && m_display != EGL_NO_DISPLAY) { + eglUnbindWaylandDisplayWL(m_display, *(WaylandServer::self()->display())); + } +} + +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(' ')); + 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); + } +} + +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()); + } + } + } +} + +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 +{ + if (qstrcmp(qgetenv("KWIN_COMPOSE"), "O2ES") == 0) { + return true; + } + return QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES; +} + +bool AbstractEglBackend::createContext() +{ + 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(), EGL_NO_CONTEXT, 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); +} + +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) +{ + const auto &buffer = pixmap->buffer(); + if (buffer.isNull()) { + if (updateFromFBO(pixmap->fbo())) { + return true; + } + return false; + } + // try Wayland loading + if (auto s = pixmap->surface()) { + s->resetTrackedDamage(); + } + if (buffer->shmBuffer()) { + return loadShmTexture(buffer); + } else { + return loadEglTexture(buffer); + } +} + +void AbstractEglTexture::updateTexture(WindowPixmap *pixmap) +{ + const auto &buffer = pixmap->buffer(); + if (buffer.isNull()) { + const auto &fbo = pixmap->fbo(); + if (!fbo.isNull()) { + if (m_texture != fbo->texture()) { + updateFromFBO(fbo); + } + return; + } + return; + } + auto s = pixmap->surface(); + if (!buffer->shmBuffer()) { + q->bind(); + EGLImageKHR image = attach(buffer); + q->unbind(); + if (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); + q->bind(); + const QRegion damage = s->trackedDamage(); + s->resetTrackedDamage(); + auto scale = s->scale(); //damage is normalised, so needs converting up to match texture + + // TODO: this should be shared with GLTexture::update + 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.rects()) { + auto scaledRect = QRect(rect.x() * scale, rect.y() * scale, rect.width() * scale, rect.height() * scale); + glTexSubImage2D(m_target, 0, scaledRect.x(), scaledRect.y(), scaledRect.width(), scaledRect.height(), + GL_BGRA_EXT, GL_UNSIGNED_BYTE, im.copy(scaledRect).bits()); + } + } else { + const QImage im = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied); + for (const QRect &rect : damage.rects()) { + auto scaledRect = QRect(rect.x() * scale, rect.y() * scale, rect.width() * scale, rect.height() * scale); + glTexSubImage2D(m_target, 0, scaledRect.x(), scaledRect.y(), scaledRect.width(), scaledRect.height(), + GL_RGBA, GL_UNSIGNED_BYTE, im.copy(scaledRect).bits()); + } + } + } else { + const QImage im = image.convertToFormat(QImage::Format_ARGB32_Premultiplied); + for (const QRect &rect : damage.rects()) { + auto scaledRect = QRect(rect.x() * scale, rect.y() * scale, rect.width() * scale, rect.height() * scale); + glTexSubImage2D(m_target, 0, scaledRect.x(), scaledRect.y(), scaledRect.width(), scaledRect.height(), + GL_BGRA, GL_UNSIGNED_BYTE, im.copy(scaledRect).bits()); + } + } + q->unbind(); +} + +bool AbstractEglTexture::loadShmTexture(const QPointer< KWayland::Server::BufferInterface > &buffer) +{ + const QImage &image = buffer->data(); + if (image.isNull()) { + return false; + } + + glGenTextures(1, &m_texture); + q->setWrapMode(GL_CLAMP_TO_EDGE); + q->setFilter(GL_LINEAR); + q->bind(); + + const QSize &size = image.size(); + // TODO: this should be shared with GLTexture(const QImage&, GLenum) + 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; +} + +bool AbstractEglTexture::loadEglTexture(const QPointer< KWayland::Server::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; +} + +EGLImageKHR AbstractEglTexture::attach(const QPointer< KWayland::Server::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; +} + +} + diff --git a/platformsupport/scenes/opengl/abstract_egl_backend.h b/platformsupport/scenes/opengl/abstract_egl_backend.h new file mode 100644 index 0000000..50d1a82 --- /dev/null +++ b/platformsupport/scenes/opengl/abstract_egl_backend.h @@ -0,0 +1,122 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_ABSTRACT_EGL_BACKEND_H +#define KWIN_ABSTRACT_EGL_BACKEND_H +#include "backend.h" +#include "texture.h" + +#include +#include +#include + +class QOpenGLFramebufferObject; + +namespace KWayland +{ +namespace Server +{ +class BufferInterface; +} +} + +namespace KWin +{ + +class KWIN_EXPORT AbstractEglBackend : public QObject, public OpenGLBackend +{ + Q_OBJECT +public: + virtual ~AbstractEglBackend(); + 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; + } + +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 unbindWaylandDisplay(); + + 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; +}; + +class KWIN_EXPORT AbstractEglTexture : public SceneOpenGLTexturePrivate +{ +public: + virtual ~AbstractEglTexture(); + 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: + bool loadShmTexture(const QPointer &buffer); + bool loadEglTexture(const QPointer &buffer); + EGLImageKHR attach(const QPointer &buffer); + bool updateFromFBO(const QSharedPointer &fbo); + 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..cb0a23d --- /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. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2009, 2010, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ +#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() +{ + return NULL; +} + +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(); + foreach (const QRect &r, region.rects()) { + 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); + } +} + +} diff --git a/platformsupport/scenes/opengl/backend.h b/platformsupport/scenes/opengl/backend.h new file mode 100644 index 0000000..483c711 --- /dev/null +++ b/platformsupport/scenes/opengl/backend.h @@ -0,0 +1,325 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2009, 2010, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_SCENE_OPENGL_BACKEND_H +#define KWIN_SCENE_OPENGL_BACKEND_H + +#include +#include + +#include + +namespace KWin +{ +class OpenGLBackend; +class OverlayWindow; +class SceneOpenGL; +class SceneOpenGLTexture; +class SceneOpenGLTexturePrivate; +class WindowPixmap; + +/** + * @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; + + /** + * @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(); + /** + * @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; + } + + /** + * @returns whether the context is surfaceless + **/ + bool isSurfaceLessContext() const { + return m_surfaceLessContext; + } + + /** + * 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); + +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; + } + + /** + * @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(); + } + + /** + * @param set whether the context is surface less + **/ + void setSurfaceLessContext(bool set) { + m_surfaceLessContext = set; + } + + /** + * 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 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; + bool m_surfaceLessContext = false; + + QList m_extensions; +}; + +} + +#endif diff --git a/platformsupport/scenes/opengl/swap_profiler.cpp b/platformsupport/scenes/opengl/swap_profiler.cpp new file mode 100644 index 0000000..619a2ae --- /dev/null +++ b/platformsupport/scenes/opengl/swap_profiler.cpp @@ -0,0 +1,57 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2009, 2010, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..8415b0a --- /dev/null +++ b/platformsupport/scenes/opengl/swap_profiler.h @@ -0,0 +1,53 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2009, 2010, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..9362720 --- /dev/null +++ b/platformsupport/scenes/opengl/texture.cpp @@ -0,0 +1,80 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2009, 2010, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..d574b0c --- /dev/null +++ b/platformsupport/scenes/opengl/texture.h @@ -0,0 +1,76 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2009, 2010, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_SCENE_OPENGL_TEXTURE_H +#define KWIN_SCENE_OPENGL_TEXTURE_H + +#include +#include + +namespace KWin +{ + +class OpenGLBackend; +class WindowPixmap; +class SceneOpenGLTexturePrivate; + +class SceneOpenGLTexture + : public GLTexture +{ +public: + SceneOpenGLTexture(OpenGLBackend *backend); + virtual ~SceneOpenGLTexture(); + + SceneOpenGLTexture & operator = (const SceneOpenGLTexture& tex); + + void discard() override final; + +protected: + bool load(WindowPixmap *pixmap); + void updateFromPixmap(WindowPixmap *pixmap); + + SceneOpenGLTexture(SceneOpenGLTexturePrivate& dd); + +private: + Q_DECLARE_PRIVATE(SceneOpenGLTexture) + + friend class OpenGLWindowPixmap; +}; + +class SceneOpenGLTexturePrivate + : public GLTexturePrivate +{ +public: + virtual ~SceneOpenGLTexturePrivate(); + + virtual bool loadTexture(WindowPixmap *pixmap) = 0; + virtual void updateTexture(WindowPixmap *pixmap); + virtual OpenGLBackend *backend() = 0; + +protected: + SceneOpenGLTexturePrivate(); + +private: + Q_DISABLE_COPY(SceneOpenGLTexturePrivate) +}; + +} + +#endif 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..263eea7 --- /dev/null +++ b/platformsupport/scenes/qpainter/backend.cpp @@ -0,0 +1,68 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..aa082bb --- /dev/null +++ b/platformsupport/scenes/qpainter/backend.h @@ -0,0 +1,108 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..da3eed3 --- /dev/null +++ b/plugins/CMakeLists.txt @@ -0,0 +1,9 @@ +add_subdirectory(kglobalaccel) +add_subdirectory(qpa) +add_subdirectory(idletime) +add_subdirectory(platforms) +add_subdirectory(scenes) + +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..791e5f7 --- /dev/null +++ b/plugins/idletime/CMakeLists.txt @@ -0,0 +1,17 @@ +set(idletime_plugin_SRCS + poller.cpp +) + +add_library(KF5IdleTimeKWinWaylandPrivatePlugin MODULE ${idletime_plugin_SRCS}) +target_link_libraries(KF5IdleTimeKWinWaylandPrivatePlugin + kwin + KF5::IdleTime + KF5::WaylandClient +) + +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..8cf20d9 --- /dev/null +++ b/plugins/idletime/poller.cpp @@ -0,0 +1,134 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..c14ba29 --- /dev/null +++ b/plugins/idletime/poller.h @@ -0,0 +1,67 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 = 0); + virtual ~Poller(); + + 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..ccfb099 --- /dev/null +++ b/plugins/kdecorations/CMakeLists.txt @@ -0,0 +1 @@ +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..760b5b8 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/CMakeLists.txt @@ -0,0 +1,65 @@ +########### 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 + 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 + decorationplugin.cpp + decorationoptions.cpp + colorhelper.cpp + ) + +add_library(decorationplugin SHARED ${decoration_plugin_SRCS}) +target_link_libraries(decorationplugin + Qt5::Quick + KDecoration2::KDecoration + KF5::ConfigWidgets +) +install(TARGETS decorationplugin DESTINATION ${QML_INSTALL_DIR}/org/kde/kwin/decoration) + +########### install files ############### + +install( FILES aurorae.knsrc DESTINATION ${CONFIG_INSTALL_DIR} ) +install( FILES + qml/aurorae.qml + qml/AuroraeButton.qml + qml/AuroraeButtonGroup.qml + qml/AuroraeMaximizeButton.qml + qml/Decoration.qml + qml/DecorationButton.qml + qml/MenuButton.qml + qml/AppMenuButton.qml + DESTINATION ${DATA_INSTALL_DIR}/kwin/aurorae ) +install( FILES + qml/Decoration.qml + qml/DecorationButton.qml + qml/MenuButton.qml + qml/AppMenuButton.qml + qml/ButtonGroup.qml + qml/qmldir + DESTINATION ${QML_INSTALL_DIR}/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..4c5879b --- /dev/null +++ b/plugins/kdecorations/aurorae/src/aurorae.cpp @@ -0,0 +1,788 @@ +/******************************************************************** +Copyright (C) 2009, 2010, 2012 Martin Gräßlin + +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, see . +*********************************************************************/ + +#include "aurorae.h" +#include "auroraetheme.h" +#include "config-kwin.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(QLatin1Literal("__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_themeName = findTheme(args); + Helper::instance().ref(); +} + +Decoration::~Decoration() +{ + Helper::instance().unref(); + if (m_context) { + m_context->makeCurrent(m_offscreenSurface.data()); + + delete m_renderControl; + delete m_view.data(); + m_fbo.reset(); + delete m_item; + + m_context->doneCurrent(); + } +} + +void Decoration::init() +{ + KDecoration2::Decoration::init(); + auto s = settings(); + connect(s.data(), &KDecoration2::DecorationSettings::reconfigured, this, &Decoration::configChanged); + + QQmlContext *context = new QQmlContext(Helper::instance().rootContext(), this); + context->setContextProperty(QStringLiteral("decoration"), this); + context->setContextProperty(QStringLiteral("decorationSettings"), s.data()); + 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)); + updateBorders(); + }; + connect(this, &Decoration::configChanged, theme, readButtonSize); + readButtonSize(); +// m_theme->setTabDragMimeType(tabDragMimeType()); + context->setContextProperty(QStringLiteral("auroraeTheme"), theme); + } + m_item = qobject_cast< QQuickItem* >(component->create(context)); + if (!m_item) { + return; + } + m_item->setParent(this); + + QVariant visualParent = property("visualParent"); + if (visualParent.isValid()) { + m_item->setParentItem(visualParent.value()); + visualParent.value()->setProperty("drawBackground", false); + } else { + m_renderControl = new QQuickRenderControl(this); + m_view = new QQuickWindow(m_renderControl); + bool usingGL = m_view->rendererInterface()->graphicsApi() == QSGRendererInterface::OpenGL; + m_view->setColor(Qt::transparent); + m_view->setFlags(Qt::FramelessWindowHint); + if (usingGL) { + // first create the context + QSurfaceFormat format; + format.setDepthBufferSize(16); + format.setStencilBufferSize(8); + m_context.reset(new QOpenGLContext); + m_context->setFormat(format); + m_context->create(); + // and the offscreen surface + m_offscreenSurface.reset(new QOffscreenSurface); + m_offscreenSurface->setFormat(m_context->format()); + m_offscreenSurface->create(); + + } + + //workaround for https://codereview.qt-project.org/#/c/207198/ +#if (QT_VERSION < QT_VERSION_CHECK(5, 10, 0)) + if (!usingGL) { + m_renderControl->sync(); + } +#endif + // delay rendering a little bit for better performance + m_updateTimer.reset(new QTimer); + m_updateTimer->setSingleShot(true); + m_updateTimer->setInterval(5); + connect(m_updateTimer.data(), &QTimer::timeout, this, + [this, usingGL] { + if (usingGL) { + if (!m_context->makeCurrent(m_offscreenSurface.data())) { + return; + } + if (m_fbo.isNull() || m_fbo->size() != m_view->size()) { + m_fbo.reset(new QOpenGLFramebufferObject(m_view->size(), QOpenGLFramebufferObject::CombinedDepthStencil)); + if (!m_fbo->isValid()) { + qCWarning(AURORAE) << "Creating FBO as render target failed"; + m_fbo.reset(); + return; + } + } + m_view->setRenderTarget(m_fbo.data()); + m_view->resetOpenGLState(); + } + + m_buffer = m_renderControl->grab(); + + m_contentRect = QRect(QPoint(0, 0), m_buffer.size()); + if (m_padding && + (m_padding->left() > 0 || m_padding->top() > 0 || m_padding->right() > 0 || m_padding->bottom() > 0) && + !client().data()->isMaximized()) { + m_contentRect = m_contentRect.adjusted(m_padding->left(), m_padding->top(), -m_padding->right(), -m_padding->bottom()); + } + updateShadow(); + + QOpenGLFramebufferObject::bindDefault(); + update(); + } + ); + auto requestUpdate = [this] { + if (m_updateTimer->isActive()) { + return; + } + m_updateTimer->start(); + }; + connect(m_renderControl, &QQuickRenderControl::renderRequested, this, requestUpdate); + connect(m_renderControl, &QQuickRenderControl::sceneChanged, this, requestUpdate); + + m_item->setParentItem(m_view->contentItem()); + + if (usingGL) { + m_context->makeCurrent(m_offscreenSurface.data()); + m_renderControl->initialize(m_context.data()); + m_context->doneCurrent(); + } + } + setupBorders(m_item); + if (m_extendedBorders) { + auto updateExtendedBorders = [this] { + setResizeOnlyBorders(*m_extendedBorders); + }; + updateExtendedBorders(); + connect(m_extendedBorders, &KWin::Borders::leftChanged, this, updateExtendedBorders); + connect(m_extendedBorders, &KWin::Borders::rightChanged, this, updateExtendedBorders); + connect(m_extendedBorders, &KWin::Borders::topChanged, this, updateExtendedBorders); + connect(m_extendedBorders, &KWin::Borders::bottomChanged, this, updateExtendedBorders); + } + connect(client().data(), &KDecoration2::DecoratedClient::maximizedChanged, this, &Decoration::updateBorders, Qt::QueuedConnection); + connect(client().data(), &KDecoration2::DecoratedClient::shadedChanged, this, &Decoration::updateBorders); + updateBorders(); + if (!m_view.isNull()) { + auto resizeWindow = [this] { + QRect rect(QPoint(0, 0), size()); + if (m_padding && !client().data()->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(client().data(), &KDecoration2::DecoratedClient::widthChanged, this, resizeWindow); + connect(client().data(), &KDecoration2::DecoratedClient::heightChanged, this, resizeWindow); + connect(client().data(), &KDecoration2::DecoratedClient::maximizedChanged, this, resizeWindow); + connect(client().data(), &KDecoration2::DecoratedClient::shadedChanged, this, resizeWindow); + resizeWindow(); + } 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 (client().data()->isMaximized() && m_maximizedBorders) { + b = m_maximizedBorders; + } + if (!b) { + return; + } + setBorders(*b); +} + +void Decoration::paint(QPainter *painter, const QRect &repaintRegion) +{ + Q_UNUSED(repaintRegion) + painter->fillRect(rect(), Qt::transparent); + painter->drawImage(rect(), m_buffer, m_contentRect); +} + +void Decoration::updateShadow() +{ + 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) && + !client().data()->isMaximized()) { + if (oldShadow.isNull()) { + updateShadow = true; + } else { + // compare padding + if (oldShadow->padding() != *m_padding) { + updateShadow = true; + } + } + 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()); + } + } +} + + +QMouseEvent Decoration::translatedMouseEvent(QMouseEvent *orig) +{ + if (!m_padding || client().data()->isMaximized()) { + orig->setAccepted(false); + return *orig; + } + QMouseEvent event(orig->type(), orig->localPos() + QPointF(m_padding->left(), m_padding->top()), orig->button(), orig->buttons(), orig->modifiers()); + event.setAccepted(false); + return event; +} + +void Decoration::hoverEnterEvent(QHoverEvent *event) +{ + if (m_view) { + event->setAccepted(false); + QCoreApplication::sendEvent(m_view.data(), event); + } + KDecoration2::Decoration::hoverEnterEvent(event); +} + +void Decoration::hoverLeaveEvent(QHoverEvent *event) +{ + if (m_view) { + event->setAccepted(false); + QCoreApplication::sendEvent(m_view.data(), event); + } + KDecoration2::Decoration::hoverLeaveEvent(event); +} + +void Decoration::hoverMoveEvent(QHoverEvent *event) +{ + if (m_view) { + QMouseEvent mouseEvent(QEvent::MouseMove, event->posF(), Qt::NoButton, Qt::NoButton, Qt::NoModifier); + QMouseEvent ev = translatedMouseEvent(&mouseEvent); + QCoreApplication::sendEvent(m_view.data(), &ev); + event->setAccepted(ev.isAccepted()); + } + KDecoration2::Decoration::hoverMoveEvent(event); +} + +void Decoration::mouseMoveEvent(QMouseEvent *event) +{ + if (m_view) { + QMouseEvent ev = translatedMouseEvent(event); + QCoreApplication::sendEvent(m_view.data(), &ev); + event->setAccepted(ev.isAccepted()); + } + KDecoration2::Decoration::mouseMoveEvent(event); +} + +void Decoration::mousePressEvent(QMouseEvent *event) +{ + if (m_view) { + QMouseEvent ev = translatedMouseEvent(event); + QCoreApplication::sendEvent(m_view.data(), &ev); + if (ev.button() == Qt::LeftButton) { + if (!m_doubleClickTimer.hasExpired(QGuiApplication::styleHints()->mouseDoubleClickInterval())) { + QMouseEvent dc(QEvent::MouseButtonDblClick, ev.localPos(), ev.windowPos(), ev.screenPos(), ev.button(), ev.buttons(), ev.modifiers()); + QCoreApplication::sendEvent(m_view.data(), &dc); + } + } + m_doubleClickTimer.invalidate(); + event->setAccepted(ev.isAccepted()); + } + KDecoration2::Decoration::mousePressEvent(event); +} + +void Decoration::mouseReleaseEvent(QMouseEvent *event) +{ + if (m_view) { + QMouseEvent ev = translatedMouseEvent(event); + QCoreApplication::sendEvent(m_view.data(), &ev); + event->setAccepted(ev.isAccepted()); + if (ev.isAccepted() && ev.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); +} + +KDecoration2::DecoratedClient *Decoration::clientPointer() const +{ + return client().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..3b28099 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/aurorae.h @@ -0,0 +1,135 @@ +/******************************************************************** +Copyright (C) 2009, 2010, 2012 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef AURORAE_H +#define AURORAE_H + +#include +#include +#include +#include + +class QOffscreenSurface; +class QOpenGLContext; +class QOpenGLFramebufferObject; +class QQmlComponent; +class QQmlEngine; +class QQuickItem; +class QQuickRenderControl; +class QQuickWindow; + +class KConfigLoader; + +namespace KWin +{ +class Borders; +} + +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()); + virtual ~Decoration(); + + 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(); + QMouseEvent translatedMouseEvent(QMouseEvent *orig); + QScopedPointer m_fbo; + QImage m_buffer; + QRect m_contentRect; //the geometry of the part of the buffer that is not a shadow when buffer was created. + QPointer m_view; + QQuickItem *m_item; + KWin::Borders *m_borders; + KWin::Borders *m_maximizedBorders; + KWin::Borders *m_extendedBorders; + KWin::Borders *m_padding; + QString m_themeName; + QQuickRenderControl *m_renderControl = nullptr; + QScopedPointer m_updateTimer; + QScopedPointer m_context; + QScopedPointer m_offscreenSurface; + 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..1e1205f --- /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 + } +} \ No newline at end of file diff --git a/plugins/kdecorations/aurorae/src/aurorae.knsrc b/plugins/kdecorations/aurorae/src/aurorae.knsrc new file mode 100644 index 0000000..73a3061 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/aurorae.knsrc @@ -0,0 +1,42 @@ +[KNewStuff3] +Name=Aurorae Window Decorations +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]=Decoración de ventanas Aurorae +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]=Aurorae Window Decorations +Name[it]=Decorazioni delle finestre Aurorae +Name[ko]=Aurorae ì°½ 장식 +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[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..285a36e --- /dev/null +++ b/plugins/kdecorations/aurorae/src/colorhelper.cpp @@ -0,0 +1,63 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..e62adb0 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/colorhelper.h @@ -0,0 +1,240 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(); + /** + * 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..b041634 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/decorationoptions.cpp @@ -0,0 +1,269 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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::Background)); + 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.dark(110)); + m_inactiveTitleBarBlendColor = wmConfig.readEntry("inactiveBlend", m_inactiveTitleBarColor.dark(110)); + m_activeFontColor = wmConfig.readEntry("activeForeground", pal.color(QPalette::Active, QPalette::HighlightedText)); + m_inactiveFontColor = wmConfig.readEntry("inactiveForeground", m_activeFontColor.dark()); + m_activeButtonColor = wmConfig.readEntry("activeTitleBtnBg", m_activeFrameColor.light(130)); + m_inactiveButtonColor = wmConfig.readEntry("inactiveTitleBtnBg", m_inactiveFrameColor.light(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().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().data(), &KDecoration2::DecoratedClient::activeChanged, this, &DecorationOptions::slotActiveChanged); + m_paletteConnection = connect(m_decoration->client().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().data()->isActive()) { + return; + } + m_active = m_decoration->client().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..f901357 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/decorationoptions.h @@ -0,0 +1,318 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~DecorationOptions(); + + 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); + virtual ~Borders(); + 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..a3575b4 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/decorationplugin.cpp @@ -0,0 +1,29 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..ead1644 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/decorationplugin.h @@ -0,0 +1,29 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); +}; + +#endif diff --git a/plugins/kdecorations/aurorae/src/kwindecoration.desktop b/plugins/kdecorations/aurorae/src/kwindecoration.desktop new file mode 100644 index 0000000..65ac174 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/kwindecoration.desktop @@ -0,0 +1,61 @@ +[Desktop Entry] +Type=ServiceType +X-KDE-ServiceType=KWin/Decoration + +Comment=KWin Window Decoration +Comment[ar]=زخارف نوافذ «نوافذك» +Comment[bs]=KWin Dekoracije prozora +Comment[ca]=Decoració de les finestres del KWin +Comment[ca@valencia]=Decoració de les finestres del 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 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 Jendela KWin +Comment[is]=KWin gluggaskreytingar +Comment[it]=Decorazione delle finestre di KWin +Comment[ja]=KWin ウィンドウの飾り +Comment[kk]=KWin терезе безендіруі +Comment[ko]=KWin ì°½ 장식 +Comment[lt]=KWin lango dekoracija +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..ba8554f --- /dev/null +++ b/plugins/kdecorations/aurorae/src/lib/auroraetheme.cpp @@ -0,0 +1,505 @@ +/* + Library for Aurorae window decoration themes. + Copyright (C) 2009, 2010, 2012 Martin Gräßlin + + 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. + +*/ + +#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 { + switch (d->borderSize) { + case KDecoration2::BorderSize::Tiny: + // TODO: this looks wrong + if (isCompositingActive()) { + left = qMin(0, (int)left - d->themeConfig.borderLeft() - d->themeConfig.paddingLeft()); + right = qMin(0, (int)right - d->themeConfig.borderRight() - d->themeConfig.paddingRight()); + bottom = qMin(0, (int)bottom - d->themeConfig.borderBottom() - d->themeConfig.paddingBottom()); + } else { + left = qMin(0, (int)left - d->themeConfig.borderLeft()); + right = qMin(0, (int)right - d->themeConfig.borderRight()); + bottom = qMin(0, (int)bottom - d->themeConfig.borderBottom()); + } + break; + case KDecoration2::BorderSize::Large: + left = right = bottom = top = 4; + break; + case KDecoration2::BorderSize::VeryLarge: + left = right = bottom = top = 8; + break; + case KDecoration2::BorderSize::Huge: + left = right = bottom = top = 12; + break; + case KDecoration2::BorderSize::VeryHuge: + left = right = bottom = top = 23; + break; + case KDecoration2::BorderSize::Oversized: + left = right = bottom = top = 36; + break; + case KDecoration2::BorderSize::Normal: + default: + left = right = bottom = top = 0; + } + const qreal title = titleHeight + d->themeConfig.titleEdgeTop() + d->themeConfig.titleEdgeBottom(); + switch ((DecorationPosition)d->themeConfig.decorationPosition()) { + case DecorationTop: + left += d->themeConfig.borderLeft(); + right += d->themeConfig.borderRight(); + bottom += d->themeConfig.borderBottom(); + top = title; + break; + case DecorationBottom: + left += d->themeConfig.borderLeft(); + right += d->themeConfig.borderRight(); + bottom = title; + top += d->themeConfig.borderTop(); + break; + case DecorationLeft: + left = title; + right += d->themeConfig.borderRight(); + bottom += d->themeConfig.borderBottom(); + top += d->themeConfig.borderTop(); + break; + case DecorationRight: + left += d->themeConfig.borderLeft(); + right = title; + bottom += d->themeConfig.borderBottom(); + top += d->themeConfig.borderTop(); + 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..f3f7b99 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/lib/auroraetheme.h @@ -0,0 +1,229 @@ +/* + Library for Aurorae window decoration themes. + Copyright (C) 2009, 2010, 2012 Martin Gräßlin + + 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. + +*/ + +#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); + virtual ~AuroraeTheme(); + // 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..357136c --- /dev/null +++ b/plugins/kdecorations/aurorae/src/lib/themeconfig.cpp @@ -0,0 +1,201 @@ +/******************************************************************** +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..2225dfa --- /dev/null +++ b/plugins/kdecorations/aurorae/src/lib/themeconfig.h @@ -0,0 +1,413 @@ +/******************************************************************** +Copyright (C) 2009 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..58841a7 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/AppMenuButton.qml @@ -0,0 +1,29 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +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..5d2968b --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/AuroraeButton.qml @@ -0,0 +1,215 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +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.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..e2eacc4 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/AuroraeButtonGroup.qml @@ -0,0 +1,60 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +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 + +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, see . +*********************************************************************/ +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..9270396 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/ButtonGroup.qml @@ -0,0 +1,101 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +import QtQuick 2.0 +import org.kde.kwin.decoration 0.1 + +Item { + function createButtons() { + for (var i=0; i + +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, see . +*********************************************************************/ +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..32d6990 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/DecorationButton.qml @@ -0,0 +1,125 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +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..2e72542 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/MenuButton.qml @@ -0,0 +1,81 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +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..5b81241 --- /dev/null +++ b/plugins/kdecorations/aurorae/src/qml/aurorae.qml @@ -0,0 +1,220 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +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 + } + 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 + } + 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..54962da --- /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: +http://techbase.kde.org/Projects/Plasma/Theme#Backgrounds + +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..8e65521 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/CMakeLists.txt @@ -0,0 +1,8 @@ +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) 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..8d3937d --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/code/CMakeLists.txt @@ -0,0 +1,9 @@ +set(plastik_plugin_SRCS + plastikbutton.cpp + plastikplugin.cpp + ) + +add_library(plastikplugin SHARED ${plastik_plugin_SRCS}) +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) 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..51720ec --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/code/plastikbutton.cpp @@ -0,0 +1,470 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin +Copyright (C) 2003-2005 Sandro Giessl + +based on the window decoration "Web": +Copyright (C) 2001 Rik Hemsley (rikkus) + +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, see . +*********************************************************************/ +#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..1e59cb6 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/code/plastikbutton.h @@ -0,0 +1,84 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_PLASTIK_BUTTON_H +#define KWIN_PLASTIK_BUTTON_H + +#include + +namespace KWin +{ + +class PlastikButtonProvider : public QQuickImageProvider +{ +public: + explicit PlastikButtonProvider(); + virtual QPixmap requestPixmap(const QString &id, QSize *size, const QSize &requestedSize); + +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..25d1125 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/code/plastikplugin.cpp @@ -0,0 +1,33 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "plastikplugin.h" +#include "plastikbutton.h" +#include + +void PlastikPlugin::registerTypes(const char *uri) +{ + Q_UNUSED(uri) +} + +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); +} + +#include "moc_plastikplugin.cpp" 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..33361a0 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/code/plastikplugin.h @@ -0,0 +1,31 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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: + virtual void registerTypes(const char *uri) override; + virtual 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..49a4b40 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/code/qmldir @@ -0,0 +1,5 @@ +module org.kde.kwin.decorations.plastik +plugin plastikplugin + +# we need to have at least one element of Qt is not able to find the plugin *shrug* +Foo 1.0 Foo.qml 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..99f1126 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/package/contents/config/main.xml @@ -0,0 +1,28 @@ + + + + + + + true + + + false + + + false + + + true + + + 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..39132f7 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/PlastikButton.qml @@ -0,0 +1,160 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +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..b2670db --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/config.ui @@ -0,0 +1,99 @@ + + + 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 titlebar text to have a 3D look with a shadow behind it. + + + Use shadowed &text + + + + + + + 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 + kcfg_titleShadow + + + +
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..3bbc2f8 --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/package/contents/ui/main.qml @@ -0,0 +1,429 @@ +/******************************************************************** +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +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); + root.titleShadow = decoration.readConfig("titleShadow", 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 + property bool titleShadow: 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 + style: root.titleShadow ? Text.Raised : Text.Normal + styleColor: colorHelper.shade(color, ColorHelper.ShadowShade) + 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..655199f --- /dev/null +++ b/plugins/kdecorations/aurorae/themes/plastik/package/metadata.desktop @@ -0,0 +1,150 @@ +[Desktop Entry] +Name=Plastik +Name[af]=Plastiek +Name[ar]=بلاستك +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]=Plastikinis QML +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[tg]=Пластик +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[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 del 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 žinoma 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]=Tema 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-Depends= +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..0466f61 --- /dev/null +++ b/plugins/kglobalaccel/kglobalaccel_plugin.cpp @@ -0,0 +1,58 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..4985a90 --- /dev/null +++ b/plugins/kglobalaccel/kglobalaccel_plugin.h @@ -0,0 +1,48 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 = 0); + virtual ~KGlobalAccelImpl(); + + 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/platforms/CMakeLists.txt b/plugins/platforms/CMakeLists.txt new file mode 100644 index 0000000..9058f0a --- /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..a857f47 --- /dev/null +++ b/plugins/platforms/drm/CMakeLists.txt @@ -0,0 +1,38 @@ +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 + 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 + remoteaccess_manager.cpp + ) +endif() + +include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/opengl) + +add_library(KWinWaylandDrmBackend MODULE ${DRM_SOURCES}) +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..66ce620 --- /dev/null +++ b/plugins/platforms/drm/drm.json @@ -0,0 +1,77 @@ +{ + "KPlugin": { + "Description": "Render through drm node.", + "Description[ca@valencia]": "Renderitza mitjançant el node del 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[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[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[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[id]": "drm", + "Name[it]": "drm", + "Name[ko]": "drm", + "Name[nl]": "drm", + "Name[nn]": "drm", + "Name[pl]": "drm", + "Name[pt]": "DRM", + "Name[pt_BR]": "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..74b7899 --- /dev/null +++ b/plugins/platforms/drm/drm_backend.cpp @@ -0,0 +1,793 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 +#endif +// KWayland +#include +#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); + handleOutputs(); +} + +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); + } + // we need to first remove all outputs + qDeleteAll(m_outputs); + m_outputs.clear(); + m_enabledOutputs.clear(); + + 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); + } +} + +Outputs DrmBackend::outputs() const +{ + return m_outputs; +} + +Outputs DrmBackend::enabledOutputs() const +{ + return m_enabledOutputs; +} + +void DrmBackend::outputWentOff() +{ + 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)->setDpms(DrmOutput::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()) { + const QPoint cp = Cursor::pos() - softwareCursorHotspot(); + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + DrmOutput *o = *it; + // only relevant in atomic mode + o->m_modesetRequested = true; + o->pageFlipped(); // TODO: Do we really need this? + o->m_crtc->blank(); + o->showCursor(); + o->moveCursor(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 (output->m_dpmsAtomicOffPending) { + output->m_modesetRequested = true; + output->dpmsAtomicOff(); + } + + 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; + } + int fd = LogindIntegration::self()->takeDevice(device->devNode()); + if (fd < 0) { + qCWarning(KWIN_DRM) << "failed to open drm device at" << device->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(); + + // 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; + + ScopedDrmPointer 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) { + drmModePlane *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."; + } + } + + ScopedDrmPointer<_drmModeRes, &drmModeFreeResources> resources(drmModeGetResources(m_fd)); + drmModeRes *res = resources.data(); + if (!resources) { + qCWarning(KWIN_DRM) << "drmModeGetResources failed"; + return; + } + + for (int i = 0; i < res->count_connectors; ++i) { + m_connectors << new DrmConnector(res->connectors[i], m_fd); + } + for (int i = 0; i < res->count_crtcs; ++i) { + m_crtcs << new DrmCrtc(res->crtcs[i], this, i); + } + + if (m_atomicModeSetting) { + auto tryAtomicInit = [] (DrmObject *obj) -> bool { + if (obj->atomicInit()) { + return false; + } else { + delete obj; + return true; + } + }; + m_connectors.erase(std::remove_if(m_connectors.begin(), m_connectors.end(), tryAtomicInit), m_connectors.end()); + m_crtcs.erase(std::remove_if(m_crtcs.begin(), m_crtcs.end(), tryAtomicInit), m_crtcs.end()); + } + + initCursor(); + updateOutputs(); + + if (m_outputs.isEmpty()) { + qCWarning(KWIN_DRM) << "No outputs, cannot render, will terminate now"; + emit initFailed(); + return; + } + + // 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); +} + +void DrmBackend::updateOutputs() +{ + if (m_fd < 0) { + return; + } + + ScopedDrmPointer<_drmModeRes, &drmModeFreeResources> resources(drmModeGetResources(m_fd)); + if (!resources) { + qCWarning(KWIN_DRM) << "drmModeGetResources failed"; + return; + } + + 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 + 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); + removed->teardown(); + } + + // now check new connections + for (DrmConnector *con : qAsConst(pendingConnectors)) { + ScopedDrmPointer<_drmModeConnector, &drmModeFreeConnector> 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)) { + ScopedDrmPointer<_drmModeEncoder, &drmModeFreeEncoder> 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 + ScopedDrmPointer<_drmModeCrtc, &drmModeFreeCrtc> 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; + connect(output, &DrmOutput::dpmsChanged, this, &DrmBackend::outputDpmsChanged); + + 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(); + if (!m_outputs.isEmpty()) { + emit screensQueried(); + } +} + +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 + (*it)->setScale(outputConfig.readEntry("Scale", 1.0)); + pos.setX(pos.x() + (*it)->geometry().width()); + } +} + +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::configurationChangeRequested(KWayland::Server::OutputConfigurationInterface *config) +{ + const auto changes = config->changes(); + bool countChanged = false; + + //process all non-disabling changes + for (auto it = changes.begin(); it != changes.end(); it++) { + KWayland::Server::OutputChangeSet *changeset = it.value(); + + auto drmoutput = findOutput(it.key()->uuid()); + if (drmoutput == nullptr) { + qCWarning(KWIN_DRM) << "Could NOT find DrmOutput matching " << it.key()->uuid(); + continue; + } + if (changeset->enabledChanged() && changeset->enabled() == KWayland::Server::OutputDeviceInterface::Enablement::Enabled) { + drmoutput->setEnabled(true); + m_enabledOutputs << drmoutput; + emit outputAdded(drmoutput); + countChanged = true; + } + drmoutput->setChanges(changeset); + } + //process any disable requests + for (auto it = changes.begin(); it != changes.end(); it++) { + KWayland::Server::OutputChangeSet *changeset = it.value(); + if (changeset->enabledChanged() && changeset->enabled() == KWayland::Server::OutputDeviceInterface::Enablement::Disabled) { + if (m_enabledOutputs.count() == 1) { + qCWarning(KWIN_DRM) << "Not disabling final screen" << it.key()->uuid(); + continue; + } + auto drmoutput = findOutput(it.key()->uuid()); + if (drmoutput == nullptr) { + qCWarning(KWIN_DRM) << "Could NOT find DrmOutput matching " << it.key()->uuid(); + continue; + } + drmoutput->setEnabled(false); + m_enabledOutputs.removeOne(drmoutput); + emit outputRemoved(drmoutput); + countChanged = true; + } + } + + if (countChanged) { + emit screensQueried(); + } else { + emit screens()->changed(); + } + // KCoreAddons needs kwayland's 2b3f9509ac1 to not crash + if (KCoreAddons::version() >= QT_VERSION_CHECK(5, 39, 0)) { + config->setApplied(); + } +} + +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; +} + +DrmOutput *DrmBackend::findOutput(const QByteArray &uuid) +{ + auto it = std::find_if(m_outputs.constBegin(), m_outputs.constEnd(), [uuid] (DrmOutput *o) { + return o->m_uuid == uuid; + }); + if (it != m_outputs.constEnd()) { + return *it; + } + return nullptr; +} + +void DrmBackend::present(DrmBuffer *buffer, DrmOutput *output) +{ + if (!buffer || buffer->bufferId() == 0) { + if (m_deleteBufferAfterPageFlip) { + delete buffer; + } + return; + } + + if (output->present(buffer)) { + m_pageFlipsPending++; + if (m_pageFlipsPending == 1 && Compositor::self()) { + Compositor::self()->aboutToSwapBuffers(); + } + } else if (m_deleteBufferAfterPageFlip) { + delete buffer; + } +} + +void DrmBackend::initCursor() +{ + m_cursorEnabled = waylandServer()->seat()->hasPointer(); + connect(waylandServer()->seat(), &KWayland::Server::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(this, &DrmBackend::cursorChanged, this, &DrmBackend::updateCursor); + connect(Cursor::self(), &Cursor::posChanged, 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); + } + } + } + markCursorAsRendered(); +} + +void DrmBackend::updateCursor() +{ + if (usesSoftwareCursor()) { + return; + } + if (isCursorHidden()) { + return; + } + const QImage &cursorImage = softwareCursor(); + if (cursorImage.isNull()) { + doHideCursor(); + return; + } + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + (*it)->updateCursor(); + } + + setCursor(); + moveCursor(); +} + +void DrmBackend::doShowCursor() +{ + updateCursor(); +} + +void DrmBackend::doHideCursor() +{ + if (!m_cursorEnabled) { + return; + } + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + (*it)->hideCursor(); + } +} + +void DrmBackend::moveCursor() +{ + if (!m_cursorEnabled || isCursorHidden()) { + 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_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::outputDpmsChanged() +{ + if (m_enabledOutputs.isEmpty()) { + return; + } + 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 HAVE_GBM + return QVector{OpenGLCompositing, QPainterCompositing}; +#else + return QVector{QPainterCompositing}; +#endif +} + +QString DrmBackend::supportInformation() const +{ + QString supportInfo; + QDebug s(&supportInfo); + s.nospace(); + s << "Name: " << "DRM" << endl; + s << "Active: " << m_active << endl; + s << "Atomic Mode Setting: " << m_atomicModeSetting << endl; + return supportInfo; +} + +} diff --git a/plugins/platforms/drm/drm_backend.h b/plugins/platforms/drm/drm_backend.h new file mode 100644 index 0000000..fe4ae33 --- /dev/null +++ b/plugins/platforms/drm/drm_backend.h @@ -0,0 +1,197 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 KWayland +{ +namespace Server +{ +class OutputInterface; +class OutputDeviceInterface; +class OutputChangeSet; +class OutputManagementInterface; +} +} + +namespace KWin +{ + +class Udev; +class UdevMonitor; + +class DrmOutput; +class DrmPlane; +class DrmCrtc; +class DrmConnector; +class GbmSurface; + + +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); + virtual ~DrmBackend(); + + void configurationChangeRequested(KWayland::Server::OutputConfigurationInterface *config) override; + Screens *createScreens(QObject *parent = nullptr) override; + QPainterBackend *createQPainterBackend() override; + OpenGLBackend* createOpenGLBackend() override; + + void init() override; + DrmDumbBuffer *createBuffer(const QSize &size); +#if HAVE_GBM + DrmSurfaceBuffer *createBuffer(const std::shared_ptr &surface); +#endif + void 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; + } + + QVector planes() const { + return m_planes; + } + QVector overlayPlanes() const { + return m_overlayPlanes; + } + + void outputWentOff(); + 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; + } + + QVector supportedCompositors() const override; + + QString supportInformation() const override; + +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(); + void updateOutputs(); + void setCursor(); + void updateCursor(); + void moveCursor(); + void initCursor(); + void outputDpmsChanged(); + void readOutputsConfiguration(); + QByteArray generateOutputConfigurationUuid() const; + DrmOutput *findOutput(quint32 connector); + DrmOutput *findOutput(const QByteArray &uuid); + 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; + // all available planes: primarys, cursors and overlays + QVector m_planes; + QVector m_overlayPlanes; + QScopedPointer m_dpmsFilter; + KWayland::Server::OutputManagementInterface *m_outputManagement = nullptr; + 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..b640c59 --- /dev/null +++ b/plugins/platforms/drm/drm_buffer.cpp @@ -0,0 +1,107 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "drm_buffer.h" + +#include "logging.h" + +// system +#include +#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..7af2ef9 --- /dev/null +++ b/plugins/platforms/drm/drm_buffer.h @@ -0,0 +1,88 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(); + + 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..cbc0d3a --- /dev/null +++ b/plugins/platforms/drm/drm_buffer_gbm.cpp @@ -0,0 +1,68 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2017 Roman Gilg +Copyright 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "drm_buffer_gbm.h" +#include "gbm_surface.h" + +#include "logging.h" + +// system +#include +#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..b9b40e8 --- /dev/null +++ b/plugins/platforms/drm/drm_buffer_gbm.h @@ -0,0 +1,67 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2017 Roman Gilg +Copyright 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(); + + 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..e2625ca --- /dev/null +++ b/plugins/platforms/drm/drm_inputeventfilter.cpp @@ -0,0 +1,115 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(quint32 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(quint32 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(quint32 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..056f779 --- /dev/null +++ b/plugins/platforms/drm/drm_inputeventfilter.h @@ -0,0 +1,56 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(); + + bool pointerEvent(QMouseEvent *event, quint32 nativeButton) override; + bool wheelEvent(QWheelEvent *event) override; + bool keyEvent(QKeyEvent *event) override; + bool touchDown(quint32 id, const QPointF &pos, quint32 time) override; + bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override; + bool touchUp(quint32 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..d2b7bb0 --- /dev/null +++ b/plugins/platforms/drm/drm_object.cpp @@ -0,0 +1,155 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Roman Gilg + +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, see . +*********************************************************************/ +#include "drm_object.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() +{ + foreach(Property* 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) { + drmModePropertyRes *prop = drmModeGetProperty(fd(), properties->props[i]); + if (!prop) { + 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, properties->prop_values[i], enumNames); + } + drmModeFreeProperty(prop); + } +} + +bool DrmObject::atomicAddProperty(drmModeAtomicReq *req, Property *property) +{ + 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; +} + +bool DrmObject::atomicPopulate(drmModeAtomicReq *req) +{ + bool ret = true; + + for (int i = 0; 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 plane" << m_id; + 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 << " has 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; + } + + int nameCount = m_enumNames.size(); + m_enumMap.resize(nameCount); + + qCDebug(KWIN_DRM).nospace() << "Test all " << prop->count_enums << + " possible enums" <<":"; + + 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) { + break; + } + } + + if (j == nameCount) { + qCWarning(KWIN_DRM).nospace() << m_propName << " has unrecognized enum '" << en->name << "'"; + } else { + 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]) { + qCDebug(KWIN_DRM) << "=>" << m_propName << "with mapped enum value" << m_enumNames[i]; + } + } + } +} + +} diff --git a/plugins/platforms/drm/drm_object.h b/plugins/platforms/drm/drm_object.h new file mode 100644 index 0000000..25b9e02 --- /dev/null +++ b/plugins/platforms/drm/drm_object.h @@ -0,0 +1,136 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Roman Gilg + +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, see . +*********************************************************************/ +#ifndef KWIN_DRM_OBJECT_H +#define KWIN_DRM_OBJECT_H + +#include +#include + +// drm +#include + + +namespace KWin +{ + +class DrmBackend; +class DrmOutput; + +class DrmObject +{ +public: + // creates drm object by its id delivered by the kernel + DrmObject(uint32_t object_id, int fd); + + virtual ~DrmObject(); + + virtual bool atomicInit() = 0; + + uint32_t id() const { + return m_id; + } + + DrmOutput *output() const { + return m_output; + } + void setOutput(DrmOutput* output) { + m_output = output; + } + + bool propHasEnum(int prop, uint64_t value) const { + auto property = m_props.at(prop); + return property ? property->hasEnum(value) : false; + } + + void 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); + } + } + + int fd() const { + return m_fd; + } + + virtual bool atomicPopulate(drmModeAtomicReq *req); + +protected: + virtual bool initProps() = 0; // only derived classes know names and quantity of properties + void setPropertyNames(QVector &&vector); + void initProp(int n, drmModeObjectProperties *properties, QVector enumNames = QVector(0)); + class Property; + bool atomicAddProperty(drmModeAtomicReq *req, Property *property); + + 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); + + uint64_t enumMap(int n) { + 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; +}; + + +} + +#endif + diff --git a/plugins/platforms/drm/drm_object_connector.cpp b/plugins/platforms/drm/drm_object_connector.cpp new file mode 100644 index 0000000..30b6e58 --- /dev/null +++ b/plugins/platforms/drm/drm_object_connector.cpp @@ -0,0 +1,80 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Roman Gilg + +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, see . +*********************************************************************/ +#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) +{ + ScopedDrmPointer<_drmModeConnector, &drmModeFreeConnector> 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"), + }); + + drmModeObjectProperties *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); + } + drmModeFreeObjectProperties(properties); + return true; +} + +bool DrmConnector::isConnected() +{ + ScopedDrmPointer<_drmModeConnector, &drmModeFreeConnector> 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..7cd2406 --- /dev/null +++ b/plugins/platforms/drm/drm_object_connector.h @@ -0,0 +1,57 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Roman Gilg + +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, see . +*********************************************************************/ +#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); + + virtual ~DrmConnector(); + + bool atomicInit(); + + enum class PropertyIndex { + CrtcId = 0, + Count + }; + + QVector encoders() { + return m_encoders; + } + + bool initProps(); + 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..85f2e65 --- /dev/null +++ b/plugins/platforms/drm/drm_object_crtc.cpp @@ -0,0 +1,121 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Roman Gilg + +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, see . +*********************************************************************/ +#include "drm_object_crtc.h" +#include "drm_backend.h" +#include "drm_output.h" +#include "drm_buffer.h" +#include "logging.h" +#include + +namespace KWin +{ + +DrmCrtc::DrmCrtc(uint32_t crtc_id, DrmBackend *backend, int resIndex) + : DrmObject(crtc_id, backend->fd()), + m_resIndex(resIndex), + m_backend(backend) +{ + ScopedDrmPointer<_drmModeCrtc, &drmModeFreeCrtc> 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"), + }); + + drmModeObjectProperties *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); + } + drmModeFreeObjectProperties(properties); + 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_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 ColorCorrect::GammaRamp &gamma) { + bool isError = drmModeCrtcSetGamma(m_backend->fd(), m_id, gamma.size, + gamma.red, gamma.green, gamma.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..c5f77e0 --- /dev/null +++ b/plugins/platforms/drm/drm_object_crtc.h @@ -0,0 +1,88 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Roman Gilg + +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, see . +*********************************************************************/ +#ifndef KWIN_DRM_OBJECT_CRTC_H +#define KWIN_DRM_OBJECT_CRTC_H + +#include "drm_object.h" + +namespace KWin +{ + +namespace ColorCorrect { +struct GammaRamp; +} + +class DrmBackend; +class DrmBuffer; +class DrmDumbBuffer; + +class DrmCrtc : public DrmObject +{ +public: + DrmCrtc(uint32_t crtc_id, DrmBackend *backend, int resIndex); + + virtual ~DrmCrtc(); + + bool atomicInit(); + + enum class PropertyIndex { + ModeId = 0, + Active, + Count + }; + + bool initProps(); + + 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 getGammaRampSize() const { + return m_gammaRampSize; + } + bool setGammaRamp(const ColorCorrect::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..929d27d --- /dev/null +++ b/plugins/platforms/drm/drm_object_plane.cpp @@ -0,0 +1,200 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Roman Gilg + +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, see . +*********************************************************************/ +#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; + ScopedDrmPointer<_drmModePlane, &drmModeFreePlane> 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::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("Primary"), + QByteArrayLiteral("Cursor"), + QByteArrayLiteral("Overlay"), + }; + + const QVector rotationNames{ + QByteArrayLiteral("rotate-0"), + QByteArrayLiteral("rotate-90"), + QByteArrayLiteral("rotate-180"), + QByteArrayLiteral("rotate-270"), + QByteArrayLiteral("reflect-x"), + QByteArrayLiteral("reflect-y") + }; + + drmModeObjectProperties *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, typeNames); + } else if (j == int(PropertyIndex::Rotation)) { + initProp(j, properties, rotationNames); + m_supportedTransformations = Transformations(); + auto testTransform = [j, this] (uint64_t value, Transformation t) { + if (propHasEnum(j, value)) { + m_supportedTransformations |= t; + } + }; + testTransform(0, Transformation::Rotate0); + testTransform(1, Transformation::Rotate90); + testTransform(2, Transformation::Rotate180); + testTransform(3, Transformation::Rotate270); + testTransform(4, Transformation::ReflectX); + testTransform(5, Transformation::ReflectY); + qCDebug(KWIN_DRM) << "Supported Transformations: " << m_supportedTransformations << " on plane " << m_id; + } else { + initProp(j, properties); + } + } + + drmModeFreeObjectProperties(properties); + 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) +{ + 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); +} + +bool DrmPlane::atomicPopulate(drmModeAtomicReq *req) +{ + bool ret = true; + + for (int i = 1; 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 plane" << m_id; + return false; + } + return true; +} + +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..cd6f739 --- /dev/null +++ b/plugins/platforms/drm/drm_object_plane.h @@ -0,0 +1,122 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Roman Gilg + +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, see . +*********************************************************************/ +#ifndef KWIN_DRM_OBJECT_PLANE_H +#define KWIN_DRM_OBJECT_PLANE_H + +#include "drm_object.h" +// drm +#include + +namespace KWin +{ + +class DrmBuffer; + +class DrmPlane : public DrmObject +{ +public: + DrmPlane(uint32_t plane_id, int fd); + + ~DrmPlane(); + + enum class PropertyIndex { + Type = 0, + SrcX, + SrcY, + SrcW, + SrcH, + CrtcX, + CrtcY, + CrtcW, + CrtcH, + FbId, + CrtcId, + Rotation, + Count + }; + + enum class TypeIndex { + Primary = 0, + Cursor, + Overlay, + Count + }; + + enum class Transformation { + Rotate0 = 1 << 0, + Rotate90 = 1 << 1, + Rotate180 = 1 << 2, + Rotate270 = 1 << 3, + ReflectX = 1 << 4, + ReflectY = 1 << 5 + }; + Q_DECLARE_FLAGS(Transformations, Transformation); + + bool atomicInit(); + bool initProps(); + 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(); + + bool atomicPopulate(drmModeAtomicReq *req); + void flipBuffer(); + void flipBufferWithDelete(); + + Transformations supportedTransformations() const { + return m_supportedTransformations; + } + +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) + +#endif + diff --git a/plugins/platforms/drm/drm_output.cpp b/plugins/platforms/drm/drm_output.cpp new file mode 100644 index 0000000..0df3b89 --- /dev/null +++ b/plugins/platforms/drm/drm_output.cpp @@ -0,0 +1,1295 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "drm_output.h" +#include "drm_backend.h" +#include "drm_object_plane.h" +#include "drm_object_crtc.h" +#include "drm_object_connector.h" + +#include + +#include "composite.h" +#include "logind.h" +#include "logging.h" +#include "main.h" +#include "orientation_sensor.h" +#include "screens_drm.h" +#include "wayland_server.h" +// KWayland +#include +#include +#include +#include +#include +#include +// KF5 +#include +#include +#include +// Qt +#include +#include +#include +// drm +#include +#include +#include + +namespace KWin +{ + +DrmOutput::DrmOutput(DrmBackend *backend) + : AbstractOutput(backend) + , m_backend(backend) +{ +} + +DrmOutput::~DrmOutput() +{ + Q_ASSERT(!m_pageFlipPending); + if (!m_deleted) { + teardown(); + } +} + +void DrmOutput::teardown() +{ + 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); + } + + m_crtc->setOutput(nullptr); + m_conn->setOutput(nullptr); + + delete m_cursor[0]; + delete m_cursor[1]; + 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() +{ + const bool ret = showCursor(m_cursor[m_cursorIndex]); + if (!ret) { + return ret; + } + + if (m_hasNewCursor) { + m_cursorIndex = (m_cursorIndex + 1) % 2; + m_hasNewCursor = false; + } + + return ret; +} + +void DrmOutput::updateCursor() +{ + QImage cursorImage = m_backend->softwareCursor(); + if (cursorImage.isNull()) { + return; + } + m_hasNewCursor = true; + QImage *c = m_cursor[m_cursorIndex]->image(); + c->fill(Qt::transparent); + c->setDevicePixelRatio(scale()); + + QPainter p; + p.begin(c); + if (orientation() == Qt::InvertedLandscapeOrientation) { + QMatrix4x4 matrix; + matrix.translate(cursorImage.width() / 2.0, cursorImage.height() / 2.0); + matrix.rotate(180.0f, 0.0f, 0.0f, 1.0f); + matrix.translate(-cursorImage.width() / 2.0, -cursorImage.height() / 2.0); + p.setWorldTransform(matrix.toTransform()); + } + p.drawImage(QPoint(0, 0), cursorImage); + p.end(); +} + +void DrmOutput::moveCursor(const QPoint &globalPos) +{ + QMatrix4x4 matrix; + QMatrix4x4 hotspotMatrix; + if (orientation() == Qt::InvertedLandscapeOrientation) { + matrix.translate(pixelSize().width() /2.0, pixelSize().height() / 2.0); + matrix.rotate(180.0f, 0.0f, 0.0f, 1.0f); + matrix.translate(-pixelSize().width() /2.0, -pixelSize().height() / 2.0); + const auto cursorSize = m_backend->softwareCursor().size(); + hotspotMatrix.translate(cursorSize.width()/2.0, cursorSize.height()/2.0); + hotspotMatrix.rotate(180.0f, 0.0f, 0.0f, 1.0f); + hotspotMatrix.translate(-cursorSize.width()/2.0, -cursorSize.height()/2.0); + } + hotspotMatrix.scale(scale()); + matrix.scale(scale()); + const auto outputGlobalPos = AbstractOutput::globalPos(); + matrix.translate(-outputGlobalPos.x(), -outputGlobalPos.y()); + const QPoint p = matrix.map(globalPos) - hotspotMatrix.map(m_backend->softwareCursorHotspot()); + drmModeMoveCursor(m_backend->fd(), m_crtc->id(), p.x(), p.y()); +} + +QSize DrmOutput::pixelSize() const +{ + auto orient = orientation(); + if (orient == Qt::PortraitOrientation || orient == Qt::InvertedPortraitOrientation) { + return QSize(m_mode.vdisplay, m_mode.hdisplay); + } + return QSize(m_mode.hdisplay, m_mode.vdisplay); +} + +void DrmOutput::setEnabled(bool enabled) +{ + if (enabled == isEnabled()) { + return; + } + if (enabled) { + setDpms(DpmsMode::On); + initOutput(); + } else { + setDpms(DpmsMode::Off); + delete waylandOutput().data(); + } + waylandOutputDevice()->setEnabled(enabled ? + KWayland::Server::OutputDeviceInterface::Enablement::Enabled : KWayland::Server::OutputDeviceInterface::Enablement::Disabled); +} + +static KWayland::Server::OutputInterface::DpmsMode toWaylandDpmsMode(DrmOutput::DpmsMode mode) +{ + using namespace KWayland::Server; + 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(); + } +} + +static DrmOutput::DpmsMode fromWaylandDpmsMode(KWayland::Server::OutputInterface::DpmsMode wlMode) +{ + using namespace KWayland::Server; + 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 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")} +}; + +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; + } + } else if (!m_crtc->blank()) { + return false; + } + + setInternal(connector->connector_type == DRM_MODE_CONNECTOR_LVDS || connector->connector_type == DRM_MODE_CONNECTOR_eDP); + + if (internal()) { + connect(kwinApp(), &Application::screensCreated, this, + [this] { + connect(screens()->orientationSensor(), &OrientationSensor::orientationChanged, this, &DrmOutput::automaticRotation); + } + ); + } + + 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; + } + setRawPhysicalSize(physicalSize); + + initOutputDevice(connector); + + setEnabled(true); + 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::initOutput() +{ + auto wlOutputDevice = waylandOutputDevice(); + Q_ASSERT(wlOutputDevice); + + auto wlOutput = waylandOutput(); + if (!wlOutput.isNull()) { + delete wlOutput.data(); + wlOutput.clear(); + } + wlOutput = waylandServer()->display()->createOutput(); + setWaylandOutput(wlOutput.data()); + createXdgOutput(); + connect(this, &DrmOutput::modeChanged, this, + [this] { + auto wlOutput = waylandOutput(); + if (wlOutput.isNull()) { + return; + } + wlOutput->setCurrentMode(QSize(m_mode.hdisplay, m_mode.vdisplay), + refreshRateForMode(&m_mode)); + auto xdg = xdgOutput(); + if (xdg) { + xdg->setLogicalSize(pixelSize() / scale()); + xdg->done(); + } + } + ); + wlOutput->setManufacturer(wlOutputDevice->manufacturer()); + wlOutput->setModel(wlOutputDevice->model()); + wlOutput->setPhysicalSize(rawPhysicalSize()); + + // set dpms + if (!m_dpms.isNull()) { + wlOutput->setDpmsSupported(true); + wlOutput->setDpmsMode(toWaylandDpmsMode(m_dpmsMode)); + connect(wlOutput.data(), &KWayland::Server::OutputInterface::dpmsModeRequested, this, + [this] (KWayland::Server::OutputInterface::DpmsMode mode) { + setDpms(fromWaylandDpmsMode(mode)); + }, Qt::QueuedConnection + ); + } + + for(const auto &mode: wlOutputDevice->modes()) { + KWayland::Server::OutputInterface::ModeFlags flags; + if (mode.flags & KWayland::Server::OutputDeviceInterface::ModeFlag::Current) { + flags |= KWayland::Server::OutputInterface::ModeFlag::Current; + } + if (mode.flags & KWayland::Server::OutputDeviceInterface::ModeFlag::Preferred) { + flags |= KWayland::Server::OutputInterface::ModeFlag::Preferred; + } + wlOutput->addMode(mode.size, flags, mode.refreshRate); + } + + wlOutput->create(); +} + +void DrmOutput::initOutputDevice(drmModeConnector *connector) +{ + auto wlOutputDevice = waylandOutputDevice(); + if (!wlOutputDevice.isNull()) { + delete wlOutputDevice.data(); + wlOutputDevice.clear(); + } + wlOutputDevice = waylandServer()->display()->createOutputDevice(); + wlOutputDevice->setUuid(m_uuid); + + if (!m_edid.eisaId.isEmpty()) { + wlOutputDevice->setManufacturer(QString::fromLatin1(m_edid.eisaId)); + } else { + wlOutputDevice->setManufacturer(i18n("unknown")); + } + + QString connectorName = s_connectorNames.value(connector->connector_type, QByteArrayLiteral("Unknown")); + QString modelName; + + if (!m_edid.monitorName.isEmpty()) { + QString model = QString::fromLatin1(m_edid.monitorName); + if (!m_edid.serialNumber.isEmpty()) { + model.append('/'); + model.append(QString::fromLatin1(m_edid.serialNumber)); + } + modelName = model; + } else if (!m_edid.serialNumber.isEmpty()) { + modelName = QString::fromLatin1(m_edid.serialNumber); + } else { + modelName = i18n("unknown"); + } + + wlOutputDevice->setModel(connectorName + QStringLiteral("-") + QString::number(connector->connector_type_id) + QStringLiteral("-") + modelName); + + wlOutputDevice->setPhysicalSize(rawPhysicalSize()); + + // read in mode information + 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]; + KWayland::Server::OutputDeviceInterface::ModeFlags deviceflags; + if (isCurrentMode(m)) { + deviceflags |= KWayland::Server::OutputDeviceInterface::ModeFlag::Current; + } + if (m->type & DRM_MODE_TYPE_PREFERRED) { + deviceflags |= KWayland::Server::OutputDeviceInterface::ModeFlag::Preferred; + } + + const auto refreshRate = refreshRateForMode(m); + + KWayland::Server::OutputDeviceInterface::Mode mode; + mode.id = i; + mode.size = QSize(m->hdisplay, m->vdisplay); + mode.flags = deviceflags; + mode.refreshRate = refreshRate; + qCDebug(KWIN_DRM) << "Adding mode: " << i << mode.size; + wlOutputDevice->addMode(mode); + } + wlOutputDevice->create(); + setWaylandOutputDevice(wlOutputDevice.data()); +} + +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; +} + +static bool verifyEdidHeader(drmModePropertyBlobPtr edid) +{ + const uint8_t *data = reinterpret_cast(edid->data); + if (data[0] != 0x00) { + return false; + } + for (int i = 1; i < 7; ++i) { + if (data[i] != 0xFF) { + return false; + } + } + if (data[7] != 0x00) { + return false; + } + return true; +} + +static QByteArray extractEisaId(drmModePropertyBlobPtr edid) +{ + /* + * From EDID standard section 3.4: + * The ID Manufacturer Name field, shown in Table 3.5, contains a 2-byte representation of the monitor's + * manufacturer. This is the same as the EISA ID. It is based on compressed ASCII, “0001=A” ... “11010=Z”. + * + * The table: + * | 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 uint8_t *data = reinterpret_cast(edid->data); + static const uint offset = 0x8; + char id[4]; + if (data[offset] >> 7) { + // bit at position 7 is not a 0 + return QByteArray(); + } + // shift two bits to right, and with 7 right most bits + id[0] = 'A' + ((data[offset] >> 2) & 0x1f) -1; + // for first byte: take last two bits and shift them 3 to left (000xx000) + // for second byte: shift 5 bits to right and take 3 right most bits (00000xxx) + // or both together + id[1] = 'A' + (((data[offset] & 0x3) << 3) | ((data[offset + 1] >> 5) & 0x7)) - 1; + // take five right most bits + id[2] = 'A' + (data[offset + 1] & 0x1f) - 1; + id[3] = '\0'; + return QByteArray(id); +} + +static void extractMonitorDescriptorDescription(drmModePropertyBlobPtr blob, DrmOutput::Edid &edid) +{ + // see section 3.10.3 + const uint8_t *data = reinterpret_cast(blob->data); + static const uint offset = 0x36; + static const uint blockLength = 18; + for (int i = 0; i < 5; ++i) { + const uint co = offset + i * blockLength; + // Flag = 0000h when block used as descriptor + if (data[co] != 0) { + continue; + } + if (data[co + 1] != 0) { + continue; + } + // Reserved = 00h when block used as descriptor + if (data[co + 2] != 0) { + continue; + } + /* + * FFh: Monitor Serial Number - Stored as ASCII, code page # 437, ≤ 13 bytes. + * FEh: ASCII String - Stored as ASCII, code page # 437, ≤ 13 bytes. + * FDh: Monitor range limits, binary coded + * FCh: Monitor name, stored as ASCII, code page # 437 + * FBh: Descriptor contains additional color point data + * FAh: Descriptor contains additional Standard Timing Identifications + * F9h - 11h: Currently undefined + * 10h: Dummy descriptor, used to indicate that the descriptor space is unused + * 0Fh - 00h: Descriptor defined by manufacturer. + */ + if (data[co + 3] == 0xfc && edid.monitorName.isEmpty()) { + edid.monitorName = QByteArray((const char *)(&data[co + 5]), 12).trimmed(); + } + if (data[co + 3] == 0xfe) { + const QByteArray id = QByteArray((const char *)(&data[co + 5]), 12).trimmed(); + if (!id.isEmpty()) { + edid.eisaId = id; + } + } + if (data[co + 3] == 0xff) { + edid.serialNumber = QByteArray((const char *)(&data[co + 5]), 12).trimmed(); + } + } +} + +static QByteArray extractSerialNumber(drmModePropertyBlobPtr edid) +{ + // see section 3.4 + const uint8_t *data = reinterpret_cast(edid->data); + static const uint offset = 0x0C; + /* + * The ID serial number is a 32-bit serial number used to differentiate between individual instances of the same model + * of monitor. Its use is optional. When used, the bit order for this field follows that shown in Table 3.6. The EDID + * structure Version 1 Revision 1 and later offer a way to represent the serial number of the monitor as an ASCII string + * in a separate descriptor block. + */ + uint32_t serialNumber = 0; + serialNumber = (uint32_t) 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 == 0) { + return QByteArray(); + } + return QByteArray::number(serialNumber); +} + +static QSize extractPhysicalSize(drmModePropertyBlobPtr edid) +{ + const uint8_t *data = reinterpret_cast(edid->data); + return QSize(data[0x15], data[0x16]) * 10; +} + +void DrmOutput::initEdid(drmModeConnector *connector) +{ + ScopedDrmPointer<_drmModePropertyBlob, &drmModeFreePropertyBlob> edid; + for (int i = 0; i < connector->count_props; ++i) { + ScopedDrmPointer<_drmModeProperty, &drmModeFreeProperty> 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; + } + + // for documentation see: http://read.pudn.com/downloads110/ebook/456020/E-EDID%20Standard.pdf + if (edid->length < 128) { + return; + } + if (!verifyEdidHeader(edid.data())) { + return; + } + m_edid.eisaId = extractEisaId(edid.data()); + m_edid.serialNumber = extractSerialNumber(edid.data()); + + // parse monitor descriptor description + extractMonitorDescriptorDescription(edid.data(), m_edid); + + m_edid.physicalSize = extractPhysicalSize(edid.data()); +} + +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; +} + +void DrmOutput::initDpms(drmModeConnector *connector) +{ + for (int i = 0; i < connector->count_props; ++i) { + ScopedDrmPointer<_drmModeProperty, &drmModeFreeProperty> property(drmModeGetProperty(m_backend->fd(), connector->props[i])); + if (!property) { + continue; + } + if (qstrcmp(property->name, "DPMS") == 0) { + m_dpms.swap(property); + break; + } + } +} + +void DrmOutput::setDpms(DrmOutput::DpmsMode mode) +{ + if (m_dpms.isNull()) { + return; + } + if (mode == m_dpmsModePending) { + qCDebug(KWIN_DRM) << "New DPMS mode equals old mode. DPMS unchanged."; + return; + } + + m_dpmsModePending = mode; + + if (m_backend->atomicModeSetting()) { + m_modesetRequested = true; + if (mode == DpmsMode::On) { + if (m_pageFlipPending) { + m_pageFlipPending = false; + Compositor::self()->bufferSwapComplete(); + } + dpmsOnHandler(); + } else { + m_dpmsAtomicOffPending = true; + if (!m_pageFlipPending) { + dpmsAtomicOff(); + } + } + } else { + if (drmModeConnectorSetProperty(m_backend->fd(), m_conn->id(), m_dpms->prop_id, uint64_t(mode)) < 0) { + m_dpmsModePending = m_dpmsMode; + qCWarning(KWIN_DRM) << "Setting DPMS failed"; + return; + } + if (mode == DpmsMode::On) { + dpmsOnHandler(); + } else { + dpmsOffHandler(); + } + m_dpmsMode = m_dpmsModePending; + } +} + +void DrmOutput::dpmsOnHandler() +{ + qCDebug(KWIN_DRM) << "DPMS mode set for output" << m_crtc->id() << "to On."; + + auto wlOutput = waylandOutput(); + if (wlOutput) { + wlOutput->setDpmsMode(toWaylandDpmsMode(m_dpmsModePending)); + } + emit dpmsChanged(); + + m_backend->checkOutputsAreOn(); + if (!m_backend->atomicModeSetting()) { + m_crtc->blank(); + } + if (Compositor *compositor = Compositor::self()) { + compositor->addRepaintFull(); + } +} + +void DrmOutput::dpmsOffHandler() +{ + qCDebug(KWIN_DRM) << "DPMS mode set for output" << m_crtc->id() << "to Off."; + + auto wlOutput = waylandOutput(); + if (wlOutput) { + wlOutput->setDpmsMode(toWaylandDpmsMode(m_dpmsModePending)); + } + emit dpmsChanged(); + + m_backend->outputWentOff(); +} + +int DrmOutput::currentRefreshRate() const +{ + auto wlOutput = waylandOutput(); + if (!wlOutput) { + return 60000; + } + return wlOutput->refreshRate(); +} + +bool DrmOutput::commitChanges() +{ + auto wlOutputDevice = waylandOutputDevice(); + Q_ASSERT(!wlOutputDevice.isNull()); + + auto changeset = changes(); + + if (changeset.isNull()) { + qCDebug(KWIN_DRM) << "no changes"; + // No changes to an output is an entirely valid thing + return true; + } + //enabledChanged is handled by drmbackend + if (changeset->modeChanged()) { + qCDebug(KWIN_DRM) << "Setting new mode:" << changeset->mode(); + wlOutputDevice->setCurrentMode(changeset->mode()); + updateMode(changeset->mode()); + } + if (changeset->transformChanged()) { + qCDebug(KWIN_DRM) << "Server setting transform: " << (int)(changeset->transform()); + transform(changeset->transform()); + } + if (changeset->positionChanged()) { + qCDebug(KWIN_DRM) << "Server setting position: " << changeset->position(); + setGlobalPos(changeset->position()); + // may just work already! + } + if (changeset->scaleChanged()) { + qCDebug(KWIN_DRM) << "Setting scale:" << changeset->scale(); + setScale(changeset->scaleF()); + } + return true; +} + +void DrmOutput::transform(KWayland::Server::OutputDeviceInterface::Transform transform) +{ + waylandOutputDevice()->setTransform(transform); + using KWayland::Server::OutputDeviceInterface; + using KWayland::Server::OutputInterface; + auto wlOutput = waylandOutput(); + + switch (transform) { + case OutputDeviceInterface::Transform::Normal: + if (m_primaryPlane) { + m_primaryPlane->setTransformation(DrmPlane::Transformation::Rotate0); + } + if (wlOutput) { + wlOutput->setTransform(OutputInterface::Transform::Normal); + } + setOrientation(Qt::PrimaryOrientation); + break; + case OutputDeviceInterface::Transform::Rotated90: + if (m_primaryPlane) { + m_primaryPlane->setTransformation(DrmPlane::Transformation::Rotate90); + } + if (wlOutput) { + wlOutput->setTransform(OutputInterface::Transform::Rotated90); + } + setOrientation(Qt::PortraitOrientation); + break; + case OutputDeviceInterface::Transform::Rotated180: + if (m_primaryPlane) { + m_primaryPlane->setTransformation(DrmPlane::Transformation::Rotate180); + } + if (wlOutput) { + wlOutput->setTransform(OutputInterface::Transform::Rotated180); + } + setOrientation(Qt::InvertedLandscapeOrientation); + break; + case OutputDeviceInterface::Transform::Rotated270: + if (m_primaryPlane) { + m_primaryPlane->setTransformation(DrmPlane::Transformation::Rotate270); + } + if (wlOutput) { + wlOutput->setTransform(OutputInterface::Transform::Rotated270); + } + setOrientation(Qt::InvertedPortraitOrientation); + break; + case OutputDeviceInterface::Transform::Flipped: + // TODO: what is this exactly? + if (wlOutput) { + wlOutput->setTransform(OutputInterface::Transform::Flipped); + } + break; + case OutputDeviceInterface::Transform::Flipped90: + // TODO: what is this exactly? + if (wlOutput) { + wlOutput->setTransform(OutputInterface::Transform::Flipped90); + } + break; + case OutputDeviceInterface::Transform::Flipped180: + // TODO: what is this exactly? + if (wlOutput) { + wlOutput->setTransform(OutputInterface::Transform::Flipped180); + } + break; + case OutputDeviceInterface::Transform::Flipped270: + // TODO: what is this exactly? + if (wlOutput) { + wlOutput->setTransform(OutputInterface::Transform::Flipped270); + } + break; + } + m_modesetRequested = true; + // the cursor might need to get rotated + updateCursor(); + showCursor(); + emit modeChanged(); +} + +void DrmOutput::updateMode(int modeIndex) +{ + // get all modes on the connector + ScopedDrmPointer<_drmModeConnector, &drmModeFreeConnector> 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; + emit modeChanged(); +} + +void DrmOutput::pageFlipped() +{ + 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(); + } +} + +bool DrmOutput::present(DrmBuffer *buffer) +{ + if (m_backend->atomicModeSetting()) { + return presentAtomically(buffer); + } else { + return presentLegacy(buffer); + } +} + +bool DrmOutput::dpmsAtomicOff() +{ + m_dpmsAtomicOffPending = 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(); + dpmsOffHandler(); + + 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; + } + + 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; + setOrientation(m_lastWorkingState.orientation); + setGlobalPos(m_lastWorkingState.globalPos); + if (m_primaryPlane) { + m_primaryPlane->setTransformation(m_lastWorkingState.planeTransformations); + } + m_modesetRequested = true; + // the cursor might need to get rotated + updateCursor(); + showCursor(); + // TODO: forward to OutputInterface and OutputDeviceInterface + emit modeChanged(); + 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.orientation = orientation(); + 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; + } + if (m_dpmsMode != DpmsMode::On) { + 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) { + dpmsOffHandler(); + } + } + + // 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; + } + 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) { + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::SrcX), 0); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::SrcY), 0); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::SrcW), m_mode.hdisplay << 16); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::SrcH), m_mode.vdisplay << 16); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::CrtcW), m_mode.hdisplay); + m_primaryPlane->setValue(int(DrmPlane::PropertyIndex::CrtcH), m_mode.vdisplay); + 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::initCursor(const QSize &cursorSize) +{ + auto createCursor = [this, cursorSize] (int index) { + m_cursor[index] = 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; +} + +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); +} + +void DrmOutput::automaticRotation() +{ + if (!m_primaryPlane) { + return; + } + const auto supportedTransformations = m_primaryPlane->supportedTransformations(); + const auto requestedTransformation = screens()->orientationSensor()->orientation(); + using KWayland::Server::OutputDeviceInterface; + OutputDeviceInterface::Transform newTransformation = OutputDeviceInterface::Transform::Normal; + switch (requestedTransformation) { + case OrientationSensor::Orientation::TopUp: + newTransformation = OutputDeviceInterface::Transform::Normal; + break; + case OrientationSensor::Orientation::TopDown: + if (!supportedTransformations.testFlag(DrmPlane::Transformation::Rotate180)) { + return; + } + newTransformation = OutputDeviceInterface::Transform::Rotated180; + break; + case OrientationSensor::Orientation::LeftUp: + if (!supportedTransformations.testFlag(DrmPlane::Transformation::Rotate90)) { + return; + } + newTransformation = OutputDeviceInterface::Transform::Rotated90; + break; + case OrientationSensor::Orientation::RightUp: + if (!supportedTransformations.testFlag(DrmPlane::Transformation::Rotate270)) { + return; + } + newTransformation = OutputDeviceInterface::Transform::Rotated270; + break; + case OrientationSensor::Orientation::FaceUp: + case OrientationSensor::Orientation::FaceDown: + case OrientationSensor::Orientation::Undefined: + // unsupported + return; + } + transform(newTransformation); + emit screens()->changed(); +} + +int DrmOutput::getGammaRampSize() const +{ + return m_crtc->getGammaRampSize(); +} + +bool DrmOutput::setGammaRamp(const ColorCorrect::GammaRamp &gamma) +{ + return m_crtc->setGammaRamp(gamma); +} + +} diff --git a/plugins/platforms/drm/drm_output.h b/plugins/platforms/drm/drm_output.h new file mode 100644 index 0000000..a257366 --- /dev/null +++ b/plugins/platforms/drm/drm_output.h @@ -0,0 +1,184 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_DRM_OUTPUT_H +#define KWIN_DRM_OUTPUT_H + +#include "abstract_output.h" +#include "drm_pointer.h" +#include "drm_object.h" +#include "drm_object_plane.h" + +#include +#include +#include +#include +#include + +#include + +namespace KWin +{ + +class DrmBackend; +class DrmBuffer; +class DrmDumbBuffer; +class DrmPlane; +class DrmConnector; +class DrmCrtc; + +class KWIN_EXPORT DrmOutput : public AbstractOutput +{ + Q_OBJECT +public: + struct Edid { + QByteArray eisaId; + QByteArray monitorName; + QByteArray serialNumber; + QSize physicalSize; + }; + ///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(const QPoint &globalPos); + bool init(drmModeConnector *connector); + bool present(DrmBuffer *buffer); + void pageFlipped(); + + /** + * Enable or disable the output. + * This differs from setDpms as it also + * removes the wl_output + * The default is on + */ + void setEnabled(bool enabled); + + bool commitChanges() override; + + QSize pixelSize() const override; + + int currentRefreshRate() const; + // 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 + }; + void setDpms(DpmsMode mode); + 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; + } + + QByteArray uuid() const { + return m_uuid; + } + + bool initCursor(const QSize &cursorSize); + + bool supportsTransformations() const; + +Q_SIGNALS: + void dpmsChanged(); + void modeChanged(); + +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(); + void initOutput(); + bool initPrimaryPlane(); + bool initCursorPlane(); + + void dpmsOnHandler(); + void dpmsOffHandler(); + bool dpmsAtomicOff(); + bool atomicReqModesetPopulate(drmModeAtomicReq *req, bool enable); + void updateMode(int modeIndex); + + void transform(KWayland::Server::OutputDeviceInterface::Transform transform); + void automaticRotation(); + + int getGammaRampSize() const override; + bool setGammaRamp(const ColorCorrect::GammaRamp &gamma) override; + + DrmBackend *m_backend; + DrmConnector *m_conn = nullptr; + DrmCrtc *m_crtc = nullptr; + bool m_lastGbm = false; + drmModeModeInfo m_mode; + Edid m_edid; + KWin::ScopedDrmPointer<_drmModeProperty, &drmModeFreeProperty> 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_dpmsAtomicOffPending = false; + bool m_modesetRequested = true; + + struct { + Qt::ScreenOrientation orientation; + drmModeModeInfo mode; + DrmPlane::Transformations planeTransformations; + QPoint globalPos; + bool valid = false; + } m_lastWorkingState; + DrmDumbBuffer *m_cursor[2] = {nullptr, nullptr}; + int m_cursorIndex = 0; + bool m_hasNewCursor = false; + bool m_internal = false; + bool m_deleted = false; +}; + +} + +Q_DECLARE_METATYPE(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..a6ec477 --- /dev/null +++ b/plugins/platforms/drm/drm_pointer.h @@ -0,0 +1,41 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_DRM_POINTER_H +#define KWIN_DRM_POINTER_H + +#include + +namespace KWin +{ + +template +struct DrmCleanup +{ + static inline void cleanup(Pointer *ptr) + { + cleanupFunc(ptr); + } +}; +template using ScopedDrmPointer = QScopedPointer>; + +} + +#endif + diff --git a/plugins/platforms/drm/egl_gbm_backend.cpp b/plugins/platforms/drm/egl_gbm_backend.cpp new file mode 100644 index 0000000..48ec495 --- /dev/null +++ b/plugins/platforms/drm/egl_gbm_backend.cpp @@ -0,0 +1,426 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 +// Qt +#include +// system +#include + +namespace KWin +{ + +EglGbmBackend::EglGbmBackend(DrmBackend *b) + : AbstractEglBackend() + , m_backend(b) +{ + // Egl is always direct rendering + setIsDirectRendering(true); + setSyncsToVBlank(true); + connect(m_backend, &DrmBackend::outputAdded, this, &EglGbmBackend::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); + } + ); +} + +EglGbmBackend::~EglGbmBackend() +{ + cleanup(); +} + +void EglGbmBackend::cleanupSurfaces() +{ + for (auto it = m_outputs.constBegin(); it != m_outputs.constEnd(); ++it) { + cleanupOutput(*it); + } + m_outputs.clear(); +} + +void EglGbmBackend::cleanupOutput(const Output &o) +{ + o.output->releaseGbm(); + + if (o.eglSurface != EGL_NO_SURFACE) { + eglDestroySurface(eglDisplay(), o.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(); + initRemotePresent(); +} + +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()); +} + +void EglGbmBackend::initRemotePresent() +{ + if (qEnvironmentVariableIsSet("KWIN_NO_REMOTE")) { + return; + } + + qCDebug(KWIN_DRM) << "Support for remote access enabled"; + m_remoteaccessManager.reset(new RemoteAccessManager); +} + +bool EglGbmBackend::resetOutput(Output &o, DrmOutput *drmOutput) +{ + o.output = drmOutput; + auto size = drmOutput->pixelSize(); + + 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) << "Create gbm surface failed"; + return false; + } + auto eglSurface = eglCreatePlatformWindowSurfaceEXT(eglDisplay(), config(), (void *)(gbmSurface->surface()), nullptr); + if (eglSurface == EGL_NO_SURFACE) { + qCCritical(KWIN_DRM) << "Create Window Surface failed"; + return false; + } else { + // destroy previous surface + if (o.eglSurface != EGL_NO_SURFACE) { + if (surface() == o.eglSurface) { + setSurface(eglSurface); + } + eglDestroySurface(eglDisplay(), o.eglSurface); + } + o.eglSurface = eglSurface; + o.gbmSurface = gbmSurface; + } + return true; +} + +void EglGbmBackend::createOutput(DrmOutput *drmOutput) +{ + Output o; + if (resetOutput(o, drmOutput)) { + 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 EglGbmBackend::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) << "Make Context Current failed"; + return false; + } + + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + qCWarning(KWIN_DRM) << "Error occurred while creating context " << error; + return false; + } + // TODO: ensure the viewport is set correctly each time + const QSize &overall = screens()->size(); + const QRect &v = output.output->geometry(); + // TODO: are the values correct? + + 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 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, chosing 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) << "choose EGL config did not return a suitable config" << count; + return false; +} + +void EglGbmBackend::present() +{ + for (auto &o: m_outputs) { + makeContextCurrent(o); + presentOnOutput(o); + } +} + +void EglGbmBackend::presentOnOutput(EglGbmBackend::Output &o) +{ + eglSwapBuffers(eglDisplay(), o.eglSurface); + o.buffer = m_backend->createBuffer(o.gbmSurface); + if(m_remoteaccessManager && gbm_surface_has_free_buffers(o.gbmSurface->surface())) { + // GBM surface is released on page flip so + // we should pass the buffer before it's presented + m_remoteaccessManager->passBuffer(o.output, o.buffer); + } + m_backend->present(o.buffer, o.output); + + if (supportsBufferAge()) { + eglQuerySurface(eglDisplay(), o.eglSurface, EGL_BUFFER_AGE_EXT, &o.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(); +} + +QRegion EglGbmBackend::prepareRenderingForScreen(int screenId) +{ + const Output &o = m_outputs.at(screenId); + makeContextCurrent(o); + if (supportsBufferAge()) { + QRegion region; + + // Note: An age of zero means the buffer contents are undefined + if (o.bufferAge > 0 && o.bufferAge <= o.damageHistory.count()) { + for (int i = 0; i < o.bufferAge - 1; i++) + region |= o.damageHistory[i]; + } else { + region = o.output->geometry(); + } + + return region; + } + return QRegion(); +} + +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 &o = m_outputs[screenId]; + if (damagedRegion.intersected(o.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(o.output->geometry()).isEmpty()) + glFlush(); + + for (auto &o: m_outputs) { + o.bufferAge = 1; + } + return; + } + presentOnOutput(o); + + // 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 (o.damageHistory.count() > 10) { + o.damageHistory.removeLast(); + } + + o.damageHistory.prepend(damagedRegion.intersected(o.output->geometry())); + } +} + +bool EglGbmBackend::usesOverlayWindow() const +{ + return false; +} + +bool EglGbmBackend::perScreenRendering() const +{ + return true; +} + +/************************************************ + * EglTexture + ************************************************/ + +EglGbmTexture::EglGbmTexture(KWin::SceneOpenGLTexture *texture, EglGbmBackend *backend) + : AbstractEglTexture(texture, backend) +{ +} + +EglGbmTexture::~EglGbmTexture() = default; + +} // namespace diff --git a/plugins/platforms/drm/egl_gbm_backend.h b/plugins/platforms/drm/egl_gbm_backend.h new file mode 100644 index 0000000..8c596e9 --- /dev/null +++ b/plugins/platforms/drm/egl_gbm_backend.h @@ -0,0 +1,101 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_EGL_GBM_BACKEND_H +#define KWIN_EGL_GBM_BACKEND_H +#include "abstract_egl_backend.h" +#include "remoteaccess_manager.h" + +#include + +struct gbm_surface; + +namespace KWin +{ +class DrmBackend; +class DrmBuffer; +class DrmOutput; +class GbmSurface; + +/** + * @brief OpenGL Backend using Egl on a GBM surface. + **/ +class EglGbmBackend : public AbstractEglBackend +{ + Q_OBJECT +public: + EglGbmBackend(DrmBackend *b); + virtual ~EglGbmBackend(); + 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(); + void initRemotePresent(); + struct Output { + DrmOutput *output = nullptr; + DrmBuffer *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; + }; + 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; + QScopedPointer m_remoteaccessManager; + friend class EglGbmTexture; +}; + +/** + * @brief Texture using an EGLImageKHR. + **/ +class EglGbmTexture : public AbstractEglTexture +{ +public: + virtual ~EglGbmTexture(); + +private: + friend class EglGbmBackend; + EglGbmTexture(SceneOpenGLTexture *texture, EglGbmBackend *backend); +}; + +} // namespace + +#endif diff --git a/plugins/platforms/drm/gbm_surface.cpp b/plugins/platforms/drm/gbm_surface.cpp new file mode 100644 index 0000000..30eb988 --- /dev/null +++ b/plugins/platforms/drm/gbm_surface.cpp @@ -0,0 +1,55 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..5eaa4c9 --- /dev/null +++ b/plugins/platforms/drm/gbm_surface.h @@ -0,0 +1,55 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..c424df8 --- /dev/null +++ b/plugins/platforms/drm/logging.cpp @@ -0,0 +1,21 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..ca1beff --- /dev/null +++ b/plugins/platforms/drm/logging.h @@ -0,0 +1,26 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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/remoteaccess_manager.cpp b/plugins/platforms/drm/remoteaccess_manager.cpp new file mode 100644 index 0000000..201fd19 --- /dev/null +++ b/plugins/platforms/drm/remoteaccess_manager.cpp @@ -0,0 +1,88 @@ +/******************************************************************** + * + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Oleg Chernovskiy + +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, see . +*********************************************************************/ +#include "drm_output.h" +#include "remoteaccess_manager.h" +#include "logging.h" +#include "drm_backend.h" +#include "../../../wayland_server.h" + +// system +#include +#include +#include +#include + +namespace KWin +{ + +RemoteAccessManager::RemoteAccessManager(QObject *parent) + : QObject(parent) +{ + if (waylandServer()) { + m_interface = waylandServer()->display()->createRemoteAccessManager(this); + m_interface->create(); + + connect(m_interface, &RemoteAccessManagerInterface::bufferReleased, + this, &RemoteAccessManager::releaseBuffer); + } +} + +RemoteAccessManager::~RemoteAccessManager() +{ + if (m_interface) { + m_interface->destroy(); + } +} + +void RemoteAccessManager::releaseBuffer(const BufferHandle *buf) +{ + int ret = close(buf->fd()); + if (Q_UNLIKELY(ret)) { + qCWarning(KWIN_DRM) << "Couldn't close released GBM fd:" << strerror(errno); + } + delete buf; +} + +void RemoteAccessManager::passBuffer(DrmOutput *output, DrmBuffer *buffer) +{ + DrmSurfaceBuffer* gbmbuf = static_cast(buffer); + + // no connected RemoteAccess instance + if (!m_interface || !m_interface->isBound()) { + return; + } + + // first buffer may be null + if (!gbmbuf || !gbmbuf->hasBo()) { + return; + } + + auto buf = new BufferHandle; + auto bo = gbmbuf->getBo(); + buf->setFd(gbm_bo_get_fd(bo)); + buf->setSize(gbm_bo_get_width(bo), gbm_bo_get_height(bo)); + buf->setStride(gbm_bo_get_stride(bo)); + buf->setFormat(gbm_bo_get_format(bo)); + + m_interface->sendBufferReady(output->waylandOutput().data(), buf); +} + +} // KWin namespace diff --git a/plugins/platforms/drm/remoteaccess_manager.h b/plugins/platforms/drm/remoteaccess_manager.h new file mode 100644 index 0000000..3a1bc69 --- /dev/null +++ b/plugins/platforms/drm/remoteaccess_manager.h @@ -0,0 +1,61 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Oleg Chernovskiy + +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, see . +*********************************************************************/ +#ifndef REMOTEACCESSMANAGER_H +#define REMOTEACCESSMANAGER_H + +// KWayland +#include +#include +// Qt +#include + +struct gbm_bo; +struct gbm_surface; + +namespace KWin +{ + +class DrmOutput; +class DrmBuffer; + +using KWayland::Server::RemoteAccessManagerInterface; +using KWayland::Server::BufferHandle; + +class RemoteAccessManager : public QObject +{ + Q_OBJECT +public: + explicit RemoteAccessManager(QObject *parent = nullptr); + virtual ~RemoteAccessManager(); + + void passBuffer(DrmOutput *output, DrmBuffer *buffer); + +signals: + void bufferNoLongerNeeded(qint32 gbm_handle); + +private: + void releaseBuffer(const BufferHandle *buf); + + RemoteAccessManagerInterface *m_interface = nullptr; +}; + +} // KWin namespace + +#endif // REMOTEACCESSMANAGER_H 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..7727ed6 --- /dev/null +++ b/plugins/platforms/drm/scene_qpainter_drm_backend.cpp @@ -0,0 +1,144 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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()); + 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()); + 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..cdb308b --- /dev/null +++ b/plugins/platforms/drm/scene_qpainter_drm_backend.h @@ -0,0 +1,60 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~DrmQPainterBackend(); + + 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..2fa1f51 --- /dev/null +++ b/plugins/platforms/drm/screens_drm.cpp @@ -0,0 +1,55 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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; + +float DrmScreens::refreshRate(int screen) const +{ + const auto enOuts = m_backend->drmEnabledOutputs(); + if (screen >= enOuts.size()) { + return Screens::refreshRate(screen); + } + return enOuts.at(screen)->currentRefreshRate() / 1000.0f; +} + +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..208b242 --- /dev/null +++ b/plugins/platforms/drm/screens_drm.h @@ -0,0 +1,43 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~DrmScreens(); + + float refreshRate(int screen) const 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..49da34a --- /dev/null +++ b/plugins/platforms/fbdev/CMakeLists.txt @@ -0,0 +1,15 @@ +set(FBDEV_SOURCES + fb_backend.cpp + logging.cpp + scene_qpainter_fb_backend.cpp +) + +add_library(KWinWaylandFbdevBackend MODULE ${FBDEV_SOURCES}) +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..871c661 --- /dev/null +++ b/plugins/platforms/fbdev/fb_backend.cpp @@ -0,0 +1,260 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "fb_backend.h" +#include "composite.h" +#include "logging.h" +#include "logind.h" +#include "scene_qpainter_fb_backend.h" +#include "screens.h" +#include "virtual_terminal.h" +#include "udev.h" +// system +#include +#include +#include +#include +// Linux +#include + +namespace KWin +{ + +FramebufferBackend::FramebufferBackend(QObject *parent) + : Platform(parent) +{ +} + +FramebufferBackend::~FramebufferBackend() +{ + unmap(); + if (m_fd >= 0) { + close(m_fd); + } +} + +Screens *FramebufferBackend::createScreens(QObject *parent) +{ + return new BasicScreens(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; + } + + // correct the color info, and try to turn on screens, assuming this is a non-primary framebuffer device + varinfo.grayscale = 0; + varinfo.transp.offset = 24; + varinfo.transp.length = 8; + varinfo.transp.msb_right = 0; + varinfo.red.offset = 16; + varinfo.red.length = 8; + varinfo.red.msb_right = 0; + varinfo.green.offset = 8; + varinfo.green.length = 8; + varinfo.green.msb_right = 0; + varinfo.blue.offset = 0; + varinfo.blue.length = 8; + varinfo.blue.msb_right = 0; + ioctl(m_fd, FBIOPUT_VSCREENINFO, &varinfo); + + // Probe the device for new screen information. + if (ioctl(m_fd, FBIOGET_VSCREENINFO, &varinfo) < 0) { + return false; + } + + m_resolution = QSize(varinfo.xres, varinfo.yres); + m_physicalSize = QSize(varinfo.width, varinfo.height); + 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; +} + +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"; + } +} + +} diff --git a/plugins/platforms/fbdev/fb_backend.h b/plugins/platforms/fbdev/fb_backend.h new file mode 100644 index 0000000..0fc9527 --- /dev/null +++ b/plugins/platforms/fbdev/fb_backend.h @@ -0,0 +1,111 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_FB_BACKEND_H +#define KWIN_FB_BACKEND_H +#include "platform.h" + +#include +#include + +namespace KWin +{ + +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); + virtual ~FramebufferBackend(); + + Screens *createScreens(QObject *parent = nullptr) override; + QPainterBackend *createQPainterBackend() override; + + QSize screenSize() const override { + return m_resolution; + } + + void init() override; + + bool isValid() const { + return m_fd >= 0; + } + + QSize size() const { + return m_resolution; + } + QSize physicalSize() const { + return m_physicalSize; + } + + 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; + } + + QVector supportedCompositors() const override { + return QVector{QPainterCompositing}; + } + +private: + void openFrameBuffer(); + bool handleScreenInfo(); + void initImageFormat(); + QSize m_resolution; + QSize m_physicalSize; + 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..762cd02 --- /dev/null +++ b/plugins/platforms/fbdev/fbdev.json @@ -0,0 +1,77 @@ +{ + "KPlugin": { + "Description": "Render to framebuffer.", + "Description[ca@valencia]": "Renderitza al «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[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[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[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[id]": "framebuffer", + "Name[it]": "framebuffer", + "Name[ko]": "framebuffer", + "Name[nl]": "framebuffer", + "Name[nn]": "biletbuffer", + "Name[pl]": "bufor klatek", + "Name[pt]": "'Framebuffer'", + "Name[pt_BR]": "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..5664208 --- /dev/null +++ b/plugins/platforms/fbdev/logging.cpp @@ -0,0 +1,21 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..ddd1f6b --- /dev/null +++ b/plugins/platforms/fbdev/logging.h @@ -0,0 +1,26 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..50e365b --- /dev/null +++ b/plugins/platforms/fbdev/scene_qpainter_fb_backend.cpp @@ -0,0 +1,91 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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->size(), QImage::Format_RGB32) + , m_backend(backend) +{ + m_renderBuffer.fill(Qt::black); + + m_backend->map(); + + m_backBuffer = QImage((uchar*)backend->mappedMemory(), + backend->bytesPerLine() / (backend->bitsPerPixel() / 8), + backend->bufferSize() / backend->bytesPerLine(), + backend->bytesPerLine(), backend->imageFormat()); + + m_backBuffer.fill(Qt::black); + connect(VirtualTerminal::self(), &VirtualTerminal::activeChanged, this, + [this] (bool active) { + if (active) { + Compositor::self()->bufferSwapComplete(); + Compositor::self()->addRepaintFull(); + } else { + Compositor::self()->aboutToSwapBuffers(); + } + } + ); +} + +FramebufferQPainterBackend::~FramebufferQPainterBackend() = default; + +QImage *FramebufferQPainterBackend::buffer() +{ + return &m_renderBuffer; +} + +bool FramebufferQPainterBackend::needsFullRepaint() const +{ + return false; +} + +void FramebufferQPainterBackend::prepareRenderingFrame() +{ +} + +void FramebufferQPainterBackend::present(int mask, const QRegion &damage) +{ + Q_UNUSED(mask) + Q_UNUSED(damage) + if (!LogindIntegration::self()->isActiveSession()) { + return; + } + QPainter p(&m_backBuffer); + p.drawImage(QPoint(0, 0), m_backend->isBGR() ? m_renderBuffer.rgbSwapped() : m_renderBuffer); +} + +bool FramebufferQPainterBackend::usesOverlayWindow() const +{ + return false; +} + +} 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..83ec47e --- /dev/null +++ b/plugins/platforms/fbdev/scene_qpainter_fb_backend.h @@ -0,0 +1,52 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~FramebufferQPainterBackend(); + + QImage *buffer() override; + bool needsFullRepaint() const override; + bool usesOverlayWindow() const override; + void prepareRenderingFrame() override; + void present(int mask, const QRegion &damage) override; + +private: + QImage m_renderBuffer; + QImage m_backBuffer; + FramebufferBackend *m_backend; +}; + +} + +#endif diff --git a/plugins/platforms/hwcomposer/CMakeLists.txt b/plugins/platforms/hwcomposer/CMakeLists.txt new file mode 100644 index 0000000..85d7c04 --- /dev/null +++ b/plugins/platforms/hwcomposer/CMakeLists.txt @@ -0,0 +1,23 @@ +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}) +target_link_libraries(KWinWaylandHwcomposerBackend + kwin + libhybris::libhardware + libhybris::hwcomposer + libhybris::hybriseglplatform + SceneOpenGLBackend +) + +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..3c96db1 --- /dev/null +++ b/plugins/platforms/hwcomposer/egl_hwcomposer_backend.cpp @@ -0,0 +1,184 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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 . +*********************************************************************/ +#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); + setBlocksForRetrace(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; + } + 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..3270104 --- /dev/null +++ b/plugins/platforms/hwcomposer/egl_hwcomposer_backend.h @@ -0,0 +1,66 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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 . +*********************************************************************/ +#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..dcfa95f --- /dev/null +++ b/plugins/platforms/hwcomposer/hwcomposer.json @@ -0,0 +1,77 @@ +{ + "KPlugin": { + "Description": "Render through hwcomposer through libhybris.", + "Description[ca@valencia]": "Renderitza mitjançant el «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[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[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[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[nl]": "hwcomposer", + "Name[nn]": "hwcomposer", + "Name[pl]": "sprzętowy kompozytor", + "Name[pt]": "Hwcomposer", + "Name[pt_BR]": "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..6cc024f --- /dev/null +++ b/plugins/platforms/hwcomposer/hwcomposer_backend.cpp @@ -0,0 +1,485 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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 . +*********************************************************************/ +#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 +#include +// Qt +#include +#include +// hybris/android +#include +#include +// linux +#include + +// based on test_hwcomposer.c from libhybris project (Apache 2 licensed) + +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(quint32 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(quint32 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(quint32 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"; + } + handleOutputs(); +} + +HwcomposerBackend::~HwcomposerBackend() +{ + if (!m_outputBlank) { + toggleBlankOutput(); + } + if (m_device) { + hwc_close_1(m_device); + } +} + +KWayland::Server::OutputInterface* HwcomposerBackend::createOutput(hwc_composer_device_1_t *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 nullptr; + } + + 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 pixel(attr_values[0], attr_values[1]); + if (pixel.isEmpty()) { + return nullptr; + } + + using namespace KWayland::Server; + OutputInterface *o = waylandServer()->display()->createOutput(waylandServer()->display()); + o->addMode(pixel, OutputInterface::ModeFlag::Current | OutputInterface::ModeFlag::Preferred, (attr_values[4] == 0) ? 60000 : 10E11/attr_values[4]); + + if (attr_values[2] != 0 && attr_values[3] != 0) { + static const qreal factor = 25.4; + m_physicalSize = QSizeF(qreal(pixel.width() * 1000) / qreal(attr_values[2]) * factor, + qreal(pixel.height() * 1000) / qreal(attr_values[3]) * factor); + o->setPhysicalSize(m_physicalSize.toSize()); + } else { + // couldn't read physical size, assume 96 dpi + o->setPhysicalSize(pixel / 3.8); + } + o->create(); + return o; +} + +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); + + initLights(); + toggleBlankOutput(); + m_filter.reset(new BacklightInputEventFilter(this)); + input()->prependInputEventFilter(m_filter.data()); + + // get display configuration + auto output = createOutput(hwcDevice); + if (!output) { + emit initFailed(); + return; + } + m_displaySize = output->pixelSize(); + m_refreshRate = output->refreshRate(); + if (m_refreshRate != 0) { + m_vsyncInterval = 1000000/m_refreshRate; + } + if (m_lights) { + using namespace KWayland::Server; + output->setDpmsSupported(true); + auto updateDpms = [this, output] { + output->setDpmsMode(m_outputBlank ? OutputInterface::DpmsMode::Off : OutputInterface::DpmsMode::On); + }; + updateDpms(); + connect(this, &HwcomposerBackend::outputBlankChanged, this, updateDpms); + connect(output, &OutputInterface::dpmsModeRequested, this, + [this] (KWayland::Server::OutputInterface::DpmsMode mode) { + if (mode == OutputInterface::DpmsMode::On) { + if (m_outputBlank) { + toggleBlankOutput(); + } + } else { + if (!m_outputBlank) { + toggleBlankOutput(); + } + } + } + ); + } + qCDebug(KWIN_HWCOMPOSER) << "Display size:" << m_displaySize; + qCDebug(KWIN_HWCOMPOSER) << "Refresh rate:" << m_refreshRate; + + emit screensQueried(); + setReady(true); +} + +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); +} + +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); + assert(err == 0); + + err = device->set(device, 1, m_list); + 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; +} + +} diff --git a/plugins/platforms/hwcomposer/hwcomposer_backend.h b/plugins/platforms/hwcomposer/hwcomposer_backend.h new file mode 100644 index 0000000..29ae5a5 --- /dev/null +++ b/plugins/platforms/hwcomposer/hwcomposer_backend.h @@ -0,0 +1,160 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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 . +*********************************************************************/ +#ifndef KWIN_HWCOMPOSER_BACKEND_H +#define KWIN_HWCOMPOSER_BACKEND_H +#include "platform.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 + +#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 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; + + QSize screenSize() const override { + return m_displaySize; + } + + HwcomposerWindow *createSurface(); + + QSize size() const { + return m_displaySize; + } + + hwc_composer_device_1_t *device() const { + return m_device; + } + int refreshRate() const { + return m_refreshRate; + } + void enableVSync(bool enable); + void waitVSync(); + void wakeVSync(); + + bool isBacklightOff() const { + return m_outputBlank; + } + + QVector supportedCompositors() const override { + return QVector{OpenGLCompositing}; + } + QSizeF physicalSize() const { + return m_physicalSize; + } + +Q_SIGNALS: + void outputBlankChanged(); + +private Q_SLOTS: + void toggleBlankOutput(); + void screenBrightnessChanged(int brightness) { + m_oldScreenBrightness = brightness; + } + +private: + void initLights(); + void toggleScreenBrightness(); + KWayland::Server::OutputInterface* createOutput(hwc_composer_device_1_t *device); + QSize m_displaySize; + hwc_composer_device_1_t *m_device = nullptr; + light_device_t *m_lights = nullptr; + bool m_outputBlank = true; + int m_refreshRate = 60000; + 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; + QSizeF m_physicalSize; +}; + +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(quint32 id, const QPointF &pos, quint32 time) override; + bool touchMotion(quint32 id, const QPointF &pos, quint32 time) override; + bool touchUp(quint32 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..38f82b1 --- /dev/null +++ b/plugins/platforms/hwcomposer/logging.cpp @@ -0,0 +1,21 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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 . +*********************************************************************/ +#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..78ef28b --- /dev/null +++ b/plugins/platforms/hwcomposer/logging.h @@ -0,0 +1,26 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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 . +*********************************************************************/ +#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..be14e7d --- /dev/null +++ b/plugins/platforms/hwcomposer/screens_hwcomposer.cpp @@ -0,0 +1,49 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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 . +*********************************************************************/ +#include "screens_hwcomposer.h" +#include "hwcomposer_backend.h" + +namespace KWin +{ + +HwcomposerScreens::HwcomposerScreens(HwcomposerBackend *backend, QObject *parent) + : BasicScreens(backend, parent) + , m_backend(backend) +{ +} + +HwcomposerScreens::~HwcomposerScreens() = default; + +float HwcomposerScreens::refreshRate(int screen) const +{ + Q_UNUSED(screen) + return m_backend->refreshRate() / 1000.0f; +} + +QSizeF HwcomposerScreens::physicalSize(int screen) const +{ + const QSizeF size = m_backend->physicalSize(); + if (size.isValid()) { + return size; + } + return Screens::physicalSize(screen); +} + +} diff --git a/plugins/platforms/hwcomposer/screens_hwcomposer.h b/plugins/platforms/hwcomposer/screens_hwcomposer.h new file mode 100644 index 0000000..384236d --- /dev/null +++ b/plugins/platforms/hwcomposer/screens_hwcomposer.h @@ -0,0 +1,43 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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 . +*********************************************************************/ +#ifndef KWIN_SCREENS_HWCOMPOSER_H +#define KWIN_SCREENS_HWCOMPOSER_H +#include "screens.h" + +namespace KWin +{ +class HwcomposerBackend; + +class HwcomposerScreens : public BasicScreens +{ + Q_OBJECT +public: + HwcomposerScreens(HwcomposerBackend *backend, QObject *parent = nullptr); + virtual ~HwcomposerScreens(); + float refreshRate(int screen) const override; + QSizeF physicalSize(int screen) const override; + +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..4f41b35 --- /dev/null +++ b/plugins/platforms/virtual/CMakeLists.txt @@ -0,0 +1,25 @@ +set(VIRTUAL_SOURCES + egl_gbm_backend.cpp + virtual_backend.cpp + virtual_output.cpp + scene_qpainter_virtual_backend.cpp + screens_virtual.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}) +target_link_libraries(KWinWaylandVirtualBackend kwin SceneQPainterBackend SceneOpenGLBackend) + +if(HAVE_GBM) + target_link_libraries(KWinWaylandVirtualBackend gbm::gbm) +endif() + +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..cce4d3d --- /dev/null +++ b/plugins/platforms/virtual/egl_gbm_backend.cpp @@ -0,0 +1,293 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "egl_gbm_backend.h" +// kwin +#include "composite.h" +#include "virtual_backend.h" +#include "options.h" +#include "screens.h" +#include "udev.h" +#include +// kwin libs +#include +#include +// Qt +#include +// system +#include +#include +#if HAVE_GBM +#include +#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(); +} + +void EglGbmBackend::initGbmDevice() +{ + if (m_backend->drmFd() != -1) { + // already initialized + return; + } + QScopedPointer udev(new Udev); + UdevDevice::Ptr device = udev->virtualGpu(); + if (!device) { + // if we don't have a virtual (vgem) device, try to find a render node + qCDebug(KWIN_VIRTUAL) << "No vgem device, looking for a render node"; + device = udev->renderNode(); + } + if (!device) { + qCDebug(KWIN_VIRTUAL) << "Neither a render node, nor a vgem device found"; + return; + } + qCDebug(KWIN_VIRTUAL) << "Found a device: " << device->devNode(); + int fd = open(device->devNode(), O_RDWR | O_CLOEXEC); + if (fd == -1) { + qCWarning(KWIN_VIRTUAL) << "Failed to open: " << device->devNode(); + return; + } + m_backend->setDrmFd(fd); +#if HAVE_GBM + auto gbmDevice = gbm_create_device(fd); + if (!gbmDevice) { + qCWarning(KWIN_VIRTUAL) << "Failed to open gbm device"; + } + m_backend->setGbmDevice(gbmDevice); +#endif +} + +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; + } + +#if HAVE_GBM + initGbmDevice(); + if (auto device = m_backend->gbmDevice()) { + display = eglGetPlatformDisplayEXT(platform, device, nullptr); + } +#endif + + if (display == EGL_NO_DISPLAY) { + qCWarning(KWIN_VIRTUAL) << "Failed to create EGLDisplay through GBM device, trying with default device"; + display = eglGetPlatformDisplayEXT(platform, EGL_DEFAULT_DISPLAY, 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(); + + 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(); + + const char* eglExtensionsCString = eglQueryString(eglDisplay(), EGL_EXTENSIONS); + const QList extensions = QByteArray::fromRawData(eglExtensionsCString, qstrlen(eglExtensionsCString)).split(' '); + if (!extensions.contains(QByteArrayLiteral("EGL_KHR_surfaceless_context"))) { + return false; + } + + if (!createContext()) { + return false; + } + setSurfaceLessContext(true); + + 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() +{ +} + +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(); + 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 + // Copyright (C) 2010 Nokia Corporation and/or its subsidiary(-ies) + // see http://qt.gitorious.org/qt/qt/blobs/master/src/opengl/qgl.cpp + if (QSysInfo::ByteOrder == QSysInfo::BigEndian) { + // OpenGL gives RGBA; Qt wants ARGB + uint *p = (uint*)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 = (uint*)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(renderedRegion) + 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.byteCount(), (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(); +} + +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..3940e9e --- /dev/null +++ b/plugins/platforms/virtual/egl_gbm_backend.h @@ -0,0 +1,75 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~EglGbmBackend(); + 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(); + void initGbmDevice(); + 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: + virtual ~EglGbmTexture(); + +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..0512d18 --- /dev/null +++ b/plugins/platforms/virtual/scene_qpainter_virtual_backend.cpp @@ -0,0 +1,89 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..b72aa8c --- /dev/null +++ b/plugins/platforms/virtual/scene_qpainter_virtual_backend.h @@ -0,0 +1,58 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~VirtualQPainterBackend(); + + 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..d2c9871 --- /dev/null +++ b/plugins/platforms/virtual/screens_virtual.cpp @@ -0,0 +1,53 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..bdc4df3 --- /dev/null +++ b/plugins/platforms/virtual/screens_virtual.h @@ -0,0 +1,45 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~VirtualScreens(); + 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..ca6a8ae --- /dev/null +++ b/plugins/platforms/virtual/virtual.json @@ -0,0 +1,78 @@ +{ + "KPlugin": { + "Description": "Render to a virtual framebuffer.", + "Description[ca@valencia]": "Renderitza a 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[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[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[ast]": "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[id]": "virtual", + "Name[it]": "virtual", + "Name[ko]": "virtual", + "Name[nl]": "virtueel", + "Name[nn]": "virtuell", + "Name[pl]": "wirtualne", + "Name[pt]": "virtual", + "Name[pt_BR]": "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..75cb8b4 --- /dev/null +++ b/plugins/platforms/virtual/virtual_backend.cpp @@ -0,0 +1,151 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 +#if HAVE_GBM +#include +#endif + +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() +{ +#if HAVE_GBM + if (m_gbmDevice) { + gbm_device_destroy(m_gbmDevice); + } +#endif + if (m_drmFd != -1) { + close(m_drmFd); + } +} + +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->setGeometry(QRect(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) +{ + Q_ASSERT(geometries.size() == 0 || geometries.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()) { + vo->setGeometry(geometries.at(i)); + } else if (!vo->geometry().isValid()) { + vo->setGeometry(QRect(QPoint(sumWidth, 0), initialWindowSize())); + sumWidth += initialWindowSize().width(); + } + 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..4b751d4 --- /dev/null +++ b/plugins/platforms/virtual/virtual_backend.h @@ -0,0 +1,94 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_VIRTUAL_BACKEND_H +#define KWIN_VIRTUAL_BACKEND_H +#include "platform.h" + +#include + +#include +#include + +class QTemporaryDir; + +struct gbm_device; + +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); + virtual ~VirtualBackend(); + 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()); + + Outputs outputs() const override; + Outputs enabledOutputs() const override; + + int drmFd() const { + return m_drmFd; + } + void setDrmFd(int fd) { + m_drmFd = fd; + } + + gbm_device *gbmDevice() const { + return m_gbmDevice; + } + void setGbmDevice(gbm_device *device) { + m_gbmDevice = device; + } + + QVector supportedCompositors() const override { + return QVector{OpenGLCompositing, QPainterCompositing}; + } + +Q_SIGNALS: + void virtualOutputsSet(bool countChanged); + +private: + QVector m_outputs; + QVector m_enabledOutputs; + + QScopedPointer m_screenshotDir; + int m_drmFd = -1; + gbm_device *m_gbmDevice = nullptr; +}; + +} + +#endif diff --git a/plugins/platforms/virtual/virtual_output.cpp b/plugins/platforms/virtual/virtual_output.cpp new file mode 100644 index 0000000..05ba336 --- /dev/null +++ b/plugins/platforms/virtual/virtual_output.cpp @@ -0,0 +1,49 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2018 Roman Gilg + +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, see . +*********************************************************************/ +#include "virtual_output.h" + +namespace KWin +{ + +VirtualOutput::VirtualOutput(QObject *parent) + : AbstractOutput() +{ + Q_UNUSED(parent); + + setScale(1.); +} + +VirtualOutput::~VirtualOutput() +{ +} + +QSize VirtualOutput::pixelSize() const +{ + return m_pixelSize; +} + +void VirtualOutput::setGeometry(const QRect &geo) +{ + m_pixelSize = geo.size(); + setRawPhysicalSize(m_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..450a868 --- /dev/null +++ b/plugins/platforms/virtual/virtual_output.h @@ -0,0 +1,64 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2018 Roman Gilg + +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, see . +*********************************************************************/ +#ifndef KWIN_VIRTUAL_OUTPUT_H +#define KWIN_VIRTUAL_OUTPUT_H + +#include "abstract_output.h" + +#include +#include + +namespace KWin +{ +class VirtualBackend; + +class VirtualOutput : public AbstractOutput +{ + Q_OBJECT + +public: + VirtualOutput(QObject *parent = nullptr); + virtual ~VirtualOutput(); + + QSize pixelSize() const override; + + void setGeometry(const QRect &geo); + + int getGammaRampSize() const override { + return m_gammaSize; + } + bool setGammaRamp(const ColorCorrect::GammaRamp &gamma) override { + Q_UNUSED(gamma); + return m_gammaResult; + } + +private: + Q_DISABLE_COPY(VirtualOutput); + friend class VirtualBackend; + + QSize m_pixelSize; + + 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..1fc8e36 --- /dev/null +++ b/plugins/platforms/wayland/CMakeLists.txt @@ -0,0 +1,24 @@ +set(WAYLAND_BACKEND_SOURCES + logging.cpp + scene_qpainter_wayland_backend.cpp + wayland_backend.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}) +target_link_libraries(KWinWaylandWaylandBackend kwin KF5::WaylandClient SceneQPainterBackend) + +if(HAVE_WAYLAND_EGL) + target_link_libraries(KWinWaylandWaylandBackend SceneOpenGLBackend Wayland::Egl) +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..16f6488 --- /dev/null +++ b/plugins/platforms/wayland/egl_wayland_backend.cpp @@ -0,0 +1,290 @@ + +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#define WL_EGL_PLATFORM 1 +#include "egl_wayland_backend.h" +// kwin +#include "composite.h" +#include "logging.h" +#include "options.h" +#include "wayland_backend.h" +#include "wayland_server.h" +#include +// kwin libs +#include +// KDE +#include +#include +// Qt +#include + +namespace KWin +{ + +EglWaylandBackend::EglWaylandBackend(Wayland::WaylandBackend *b) + : AbstractEglBackend() + , m_bufferAge(0) + , m_wayland(b) + , m_overlay(NULL) +{ + if (!m_wayland) { + setFailed("Wayland Backend has not been created"); + return; + } + qCDebug(KWIN_WAYLAND_BACKEND) << "Connected to Wayland display?" << (m_wayland->display() ? "yes" : "no" ); + if (!m_wayland->display()) { + setFailed("Could not connect to Wayland compositor"); + return; + } + connect(m_wayland, SIGNAL(shellSurfaceSizeChanged(QSize)), SLOT(overlaySizeChanged(QSize))); + // Egl is always direct rendering + setIsDirectRendering(true); +} + +EglWaylandBackend::~EglWaylandBackend() +{ + cleanup(); + if (m_overlay) { + wl_egl_window_destroy(m_overlay); + } +} + +bool EglWaylandBackend::initializeEgl() +{ + initClientExtensions(); + EGLDisplay display = m_wayland->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_wayland->display(), nullptr); + } else { + display = eglGetDisplay(m_wayland->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; + } + + if (!m_wayland->surface()) { + return false; + } + + const QSize &size = m_wayland->shellSurfaceSize(); + auto s = m_wayland->surface(); + connect(s, &KWayland::Client::Surface::frameRendered, Compositor::self(), &Compositor::bufferSwapComplete); + m_overlay = wl_egl_window_create(*s, size.width(), size.height()); + if (!m_overlay) { + qCCritical(KWIN_WAYLAND_BACKEND) << "Creating Wayland Egl window failed"; + return false; + } + + EGLSurface surface = EGL_NO_SURFACE; + if (m_havePlatformBase) + surface = eglCreatePlatformWindowSurfaceEXT(eglDisplay(), config(), (void *) m_overlay, nullptr); + else + surface = eglCreateWindowSurface(eglDisplay(), config(), m_overlay, nullptr); + + if (surface == EGL_NO_SURFACE) { + qCCritical(KWIN_WAYLAND_BACKEND) << "Create Window Surface failed"; + return false; + } + setSurface(surface); + + return makeContextCurrent(); +} + +bool EglWaylandBackend::makeContextCurrent() +{ + if (eglMakeCurrent(eglDisplay(), surface(), surface(), 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; + } + 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() +{ + m_wayland->surface()->setupFrameCallback(); + Compositor::self()->aboutToSwapBuffers(); + + if (supportsBufferAge()) { + eglSwapBuffers(eglDisplay(), surface()); + eglQuerySurface(eglDisplay(), surface(), EGL_BUFFER_AGE_EXT, &m_bufferAge); + setLastDamage(QRegion()); + return; + } else { + eglSwapBuffers(eglDisplay(), surface()); + setLastDamage(QRegion()); + } +} + +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 + m_bufferAge = 0; +} + +SceneOpenGLTexturePrivate *EglWaylandBackend::createBackendTexture(SceneOpenGLTexture *texture) +{ + return new EglWaylandTexture(texture, this); +} + +QRegion EglWaylandBackend::prepareRenderingFrame() +{ + if (!lastDamage().isEmpty()) + present(); + QRegion repaint; + if (supportsBufferAge()) + repaint = accumulatedDamageHistory(m_bufferAge); + eglWaitNative(EGL_CORE_NATIVE_ENGINE); + startRenderTimer(); + return repaint; +} + +void EglWaylandBackend::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(); + } + + // Save the damaged region to history + if (supportsBufferAge()) + addToDamageHistory(damagedRegion); +} + +void EglWaylandBackend::overlaySizeChanged(const QSize &size) +{ + wl_egl_window_resize(m_overlay, size.width(), size.height(), 0, 0); +} + +bool EglWaylandBackend::usesOverlayWindow() const +{ + return false; +} + +/************************************************ + * EglTexture + ************************************************/ + +EglWaylandTexture::EglWaylandTexture(KWin::SceneOpenGLTexture *texture, KWin::EglWaylandBackend *backend) + : AbstractEglTexture(texture, backend) +{ +} + +EglWaylandTexture::~EglWaylandTexture() = default; + +} // namespace diff --git a/plugins/platforms/wayland/egl_wayland_backend.h b/plugins/platforms/wayland/egl_wayland_backend.h new file mode 100644 index 0000000..e42da7d --- /dev/null +++ b/plugins/platforms/wayland/egl_wayland_backend.h @@ -0,0 +1,95 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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; +} + +/** + * @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(Wayland::WaylandBackend *b); + virtual ~EglWaylandBackend(); + virtual void screenGeometryChanged(const QSize &size); + virtual SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; + virtual QRegion prepareRenderingFrame(); + virtual void endRenderingFrame(const QRegion &renderedRegion, const QRegion &damagedRegion); + virtual bool usesOverlayWindow() const override; + void init() override; + +protected: + virtual void present(); + +private Q_SLOTS: + void overlaySizeChanged(const QSize &size); + +private: + bool initializeEgl(); + bool initBufferConfigs(); + bool initRenderingContext(); + bool makeContextCurrent(); + int m_bufferAge; + Wayland::WaylandBackend *m_wayland; + wl_egl_window *m_overlay; + bool m_havePlatformBase; + friend class EglWaylandTexture; +}; + +/** + * @brief Texture using an EGLImageKHR. + **/ +class EglWaylandTexture : public AbstractEglTexture +{ +public: + virtual ~EglWaylandTexture(); + +private: + friend class EglWaylandBackend; + EglWaylandTexture(SceneOpenGLTexture *texture, EglWaylandBackend *backend); +}; + +} // namespace + +#endif // KWIN_EGL_ON_X_BACKEND_H diff --git a/plugins/platforms/wayland/logging.cpp b/plugins/platforms/wayland/logging.cpp new file mode 100644 index 0000000..3947cfb --- /dev/null +++ b/plugins/platforms/wayland/logging.cpp @@ -0,0 +1,21 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..8225f94 --- /dev/null +++ b/plugins/platforms/wayland/logging.h @@ -0,0 +1,26 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..ee6f0ba --- /dev/null +++ b/plugins/platforms/wayland/scene_qpainter_wayland_backend.cpp @@ -0,0 +1,134 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013, 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "scene_qpainter_wayland_backend.h" +#include "composite.h" +#include "logging.h" +#include "wayland_backend.h" +#include +#include +#include + +namespace KWin +{ + +WaylandQPainterBackend::WaylandQPainterBackend(Wayland::WaylandBackend *b) + : QPainterBackend() + , m_backend(b) + , m_needsFullRepaint(true) + , m_backBuffer(QImage(QSize(), QImage::Format_RGB32)) + , m_buffer() +{ + connect(b->shmPool(), SIGNAL(poolResized()), SLOT(remapBuffer())); + connect(b, &Wayland::WaylandBackend::shellSurfaceSizeChanged, + this, &WaylandQPainterBackend::screenGeometryChanged); + connect(b->surface(), &KWayland::Client::Surface::frameRendered, + Compositor::self(), &Compositor::bufferSwapComplete); +} + +WaylandQPainterBackend::~WaylandQPainterBackend() +{ + if (m_buffer) { + m_buffer.toStrongRef()->setUsed(false); + } +} + +bool WaylandQPainterBackend::usesOverlayWindow() const +{ + return false; +} + +void WaylandQPainterBackend::present(int mask, const QRegion &damage) +{ + Q_UNUSED(mask) + if (m_backBuffer.isNull()) { + return; + } + Compositor::self()->aboutToSwapBuffers(); + m_needsFullRepaint = false; + auto s = m_backend->surface(); + s->attachBuffer(m_buffer); + s->damage(damage); + s->commit(); +} + +void WaylandQPainterBackend::screenGeometryChanged(const QSize &size) +{ + Q_UNUSED(size) + if (!m_buffer) { + return; + } + m_buffer.toStrongRef()->setUsed(false); + m_buffer.clear(); +} + +QImage *WaylandQPainterBackend::buffer() +{ + return &m_backBuffer; +} + +void WaylandQPainterBackend::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_backend->shellSurfaceSize()); + m_buffer = m_backend->shmPool()->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); + m_needsFullRepaint = true; + qCDebug(KWIN_WAYLAND_BACKEND) << "Created a new back buffer"; +} + +void WaylandQPainterBackend::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 our back buffer"; +} + +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..3e81ef9 --- /dev/null +++ b/plugins/platforms/wayland/scene_qpainter_wayland_backend.h @@ -0,0 +1,68 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013, 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_SCENE_QPAINTER_WAYLAND_BACKEND_H +#define KWIN_SCENE_QPAINTER_WAYLAND_BACKEND_H + +#include + +#include +#include +#include + +namespace KWayland +{ +namespace Client +{ +class Buffer; +} +} + +namespace KWin +{ +namespace Wayland +{ +class WaylandBackend; +} + +class WaylandQPainterBackend : public QObject, public QPainterBackend +{ + Q_OBJECT +public: + explicit WaylandQPainterBackend(Wayland::WaylandBackend *b); + virtual ~WaylandQPainterBackend(); + + virtual void present(int mask, const QRegion& damage) override; + virtual bool usesOverlayWindow() const override; + virtual void screenGeometryChanged(const QSize &size) override; + virtual QImage *buffer() override; + virtual void prepareRenderingFrame() override; + virtual bool needsFullRepaint() const override; +private Q_SLOTS: + void remapBuffer(); +private: + Wayland::WaylandBackend *m_backend; + bool m_needsFullRepaint; + QImage m_backBuffer; + QWeakPointer m_buffer; +}; + +} + +#endif diff --git a/plugins/platforms/wayland/wayland.json b/plugins/platforms/wayland/wayland.json new file mode 100644 index 0000000..8fb1b3c --- /dev/null +++ b/plugins/platforms/wayland/wayland.json @@ -0,0 +1,77 @@ +{ + "KPlugin": { + "Description": "Render to a nested window on running Wayland compositor.", + "Description[ca@valencia]": "Renderitza a 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 jendela tersarang pada Wayland compositor yang berjalan.", + "Description[it]": "Resa in una finestra nidificata su compositore Wayland in esecuzione.", + "Description[ko]": "Wayland 컴포지터에서 실행 중인 창에 렌더링합니다.", + "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[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[id]": "wayland", + "Name[it]": "wayland", + "Name[ko]": "wayland", + "Name[nl]": "wayland", + "Name[nn]": "wayland", + "Name[pl]": "wayland", + "Name[pt]": "Wayland", + "Name[pt_BR]": "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..e06881f --- /dev/null +++ b/plugins/platforms/wayland/wayland_backend.cpp @@ -0,0 +1,668 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +// own +#include "wayland_backend.h" +#include +// KWin +#include "cursor.h" +#include "logging.h" +#include "main.h" +#include "scene_qpainter_wayland_backend.h" +#include "screens.h" +#include "wayland_server.h" +#include "wayland_cursor_theme.h" +#if HAVE_WAYLAND_EGL +#include "egl_wayland_backend.h" +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +// Qt +#include +#include +// Wayland +#include + +#include + +namespace KWin +{ +namespace Wayland +{ + +using namespace KWayland::Client; + +WaylandSeat::WaylandSeat(wl_seat *seat, WaylandBackend *backend) + : QObject(NULL) + , m_seat(new Seat(this)) + , m_pointer(NULL) + , m_keyboard(NULL) + , m_touch(nullptr) + , m_cursor(NULL) + , m_enteredSerial(0) + , m_backend(backend) + , m_installCursor(false) +{ + 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->togglePointerConfinement(); + } + 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) { + m_enteredSerial = serial; + if (!m_installCursor) { + // explicitly hide cursor + m_pointer->hideCursor(); + } + } + ); + connect(m_pointer, &Pointer::motion, this, + [this](const QPointF &relativeToSurface, quint32 time) { + m_backend->pointerMotion(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(); + } + } + ); + 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 KWayland::Server; + 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); + } +} + +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::installCursorImage(wl_buffer *image, const QSize &size, const QPoint &hotSpot) +{ + if (!m_installCursor) { + return; + } + if (!m_pointer || !m_pointer->isValid()) { + return; + } + if (!m_cursor) { + m_cursor = m_backend->compositor()->createSurface(this); + } + if (!m_cursor || !m_cursor->isValid()) { + return; + } + m_pointer->setCursor(m_cursor, hotSpot); + m_cursor->attachBuffer(image); + m_cursor->damage(QRect(QPoint(0,0), size)); + m_cursor->commit(Surface::CommitFlag::None); + m_backend->flush(); +} + +void WaylandSeat::installCursorImage(const QImage &image, const QPoint &hotSpot) +{ + if (image.isNull()) { + installCursorImage(nullptr, QSize(), QPoint()); + return; + } + installCursorImage(*(m_backend->shmPool()->createBuffer(image).data()), image.size(), hotSpot); +} + +void WaylandSeat::setInstallCursor(bool install) +{ + // TODO: remove, add? + m_installCursor = install; +} + +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_shell(new Shell(this)) + , m_surface(nullptr) + , m_shellSurface(NULL) + , m_seat() + , m_shm(new ShmPool(this)) + , m_connectionThreadObject(new ConnectionThread(nullptr)) + , m_connectionThread(nullptr) +{ + connect(this, &WaylandBackend::connectionFailed, this, &WaylandBackend::initFailed); + connect(this, &WaylandBackend::shellSurfaceSizeChanged, this, &WaylandBackend::screenSizeChanged); +} + +WaylandBackend::~WaylandBackend() +{ + if (m_pointerConstraints) { + m_pointerConstraints->release(); + } + if (m_xdgShellSurface) { + m_xdgShellSurface->release(); + } + if (m_shellSurface) { + m_shellSurface->release(); + } + if (m_surface) { + m_surface->release(); + } + if (m_xdgShell) { + m_xdgShell->release(); + } + m_shell->release(); + m_compositor->release(); + m_registry->release(); + m_seat.reset(); + m_shm->release(); + m_eventQueue->release(); + + m_connectionThreadObject->deleteLater(); + m_connectionThread->quit(); + m_connectionThread->wait(); + + 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::shellAnnounced, this, + [this](quint32 name) { + m_shell->setup(m_registry->bindShell(name, 1)); + } + ); + connect(m_registry, &Registry::seatAnnounced, this, + [this](quint32 name) { + if (Application::usesLibinput()) { + return; + } + m_seat.reset(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::pointerConstraintsUnstableV1Announced, this, + [this](quint32 name, quint32 version) { + if (m_pointerConstraints) { + return; + } + m_pointerConstraints = m_registry->createPointerConstraints(name, version, this); + updateWindowTitle(); + } + ); + connect(m_registry, &Registry::interfacesAnnounced, this, &WaylandBackend::createSurface); + 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.data()); + m_seat->installGesturesInterface(gesturesInterface); + } + ); + if (!deviceIdentifier().isEmpty()) { + m_connectionThreadObject->setSocketName(deviceIdentifier()); + } + connect(this, &WaylandBackend::cursorChanged, this, + [this] { + if (m_seat.isNull() || !m_seat->isInstallCursor()) { + return; + } + m_seat->installCursorImage(softwareCursor(), softwareCursorHotspot()); + markCursorAsRendered(); + } + ); + initConnection(); +} + +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(); + m_seat.reset(); + m_shm->destroy(); + if (m_xdgShellSurface) { + m_xdgShellSurface->destroy(); + delete m_xdgShellSurface; + m_xdgShellSurface = nullptr; + } + if (m_shellSurface) { + m_shellSurface->destroy(); + delete m_shellSurface; + m_shellSurface = nullptr; + } + if (m_surface) { + m_surface->destroy(); + delete m_surface; + m_surface = nullptr; + } + if (m_shell) { + m_shell->destroy(); + } + if (m_xdgShell) { + m_xdgShell->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::createSurface() +{ + m_surface = m_compositor->createSurface(this); + if (!m_surface || !m_surface->isValid()) { + qCCritical(KWIN_WAYLAND_BACKEND) << "Creating Wayland Surface failed"; + return; + } + using namespace KWayland::Client; + auto iface = m_registry->interface(Registry::Interface::ServerSideDecorationManager); + if (iface.name != 0) { + auto manager = m_registry->createServerSideDecorationManager(iface.name, iface.version, this); + auto decoration = manager->create(m_surface, this); + connect(decoration, &ServerSideDecoration::modeChanged, this, + [this, decoration] { + if (decoration->mode() != ServerSideDecoration::Mode::Server) { + decoration->requestMode(ServerSideDecoration::Mode::Server); + } + } + ); + } + if (m_seat) { + m_seat->setInstallCursor(true); + } + // check for xdg shell + auto xdgIface = m_registry->interface(Registry::Interface::XdgShellUnstableV6); + if (xdgIface.name != 0) { + m_xdgShell = m_registry->createXdgShell(xdgIface.name, xdgIface.version, this); + if (m_xdgShell && m_xdgShell->isValid()) { + m_xdgShellSurface = m_xdgShell->createSurface(m_surface, this); + connect(m_xdgShellSurface, &XdgShellSurface::closeRequested, qApp, &QCoreApplication::quit); + setupSurface(m_xdgShellSurface); + return; + } + } + if (m_shell->isValid()) { + m_shellSurface = m_shell->createSurface(m_surface, this); + setupSurface(m_shellSurface); + m_shellSurface->setToplevel(); + } +} + +template +void WaylandBackend::setupSurface(T *surface) +{ + connect(surface, &T::sizeChanged, this, &WaylandBackend::shellSurfaceSizeChanged); + surface->setSize(initialWindowSize()); + updateWindowTitle(); + setReady(true); + emit screensQueried(); +} + +QSize WaylandBackend::shellSurfaceSize() const +{ + if (m_shellSurface) { + return m_shellSurface->size(); + } + if (m_xdgShellSurface) { + return m_xdgShellSurface->size(); + } + return QSize(); +} + +Screens *WaylandBackend::createScreens(QObject *parent) +{ + return new BasicScreens(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::flush() +{ + if (m_connectionThreadObject) { + m_connectionThreadObject->flush(); + } +} + +void WaylandBackend::togglePointerConfinement() +{ + if (!m_pointerConstraints) { + return; + } + if (!m_seat) { + return; + } + auto p = m_seat->pointer(); + if (!p) { + return; + } + if (!m_surface) { + return; + } + if (m_pointerConfinement && m_isPointerConfined) { + delete m_pointerConfinement; + m_pointerConfinement = nullptr; + m_isPointerConfined = false; + updateWindowTitle(); + flush(); + return; + } else if (m_pointerConfinement) { + return; + } + m_pointerConfinement = m_pointerConstraints->confinePointer(m_surface, p, nullptr, PointerConstraints::LifeTime::Persistent, this); + connect(m_pointerConfinement, &ConfinedPointer::confined, this, + [this] { + m_isPointerConfined = true; + updateWindowTitle(); + } + ); + connect(m_pointerConfinement, &ConfinedPointer::unconfined, this, + [this] { + m_isPointerConfined = false; + updateWindowTitle(); + } + ); + updateWindowTitle(); + flush(); +} + +void WaylandBackend::updateWindowTitle() +{ + if (!m_xdgShellSurface) { + return; + } + QString grab; + if (m_isPointerConfined) { + grab = i18n("Press right control to ungrab pointer"); + } else { + if (!m_pointerConfinement && m_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)", waylandServer()->display()->socketName()); + if (grab.isEmpty()) { + m_xdgShellSurface->setTitle(title); + } else { + m_xdgShellSurface->setTitle(title + QStringLiteral(" - ") + grab); + } +} + +QVector WaylandBackend::supportedCompositors() const +{ +#if HAVE_WAYLAND_EGL + return QVector{OpenGLCompositing, QPainterCompositing}; +#else + return QVector{QPainterCompositing}; +#endif +} + + +} + +} // KWin diff --git a/plugins/platforms/wayland/wayland_backend.h b/plugins/platforms/wayland/wayland_backend.h new file mode 100644 index 0000000..898479c --- /dev/null +++ b/plugins/platforms/wayland/wayland_backend.h @@ -0,0 +1,208 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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; + +namespace KWayland +{ +namespace Client +{ +class Buffer; +class ShmPool; +class Compositor; +class ConfinedPointer; +class ConnectionThread; +class EventQueue; +class Keyboard; +class Pointer; +class PointerConstraints; +class PointerGestures; +class PointerSwipeGesture; +class PointerPinchGesture; +class Registry; +class Seat; +class Shell; +class ShellSurface; +class Surface; +class Touch; +class XdgShell; +class XdgShellSurface; +} +} + +namespace KWin +{ +class WaylandCursorTheme; + +namespace Wayland +{ + +class WaylandBackend; +class WaylandSeat; + +class WaylandSeat : public QObject +{ + Q_OBJECT +public: + WaylandSeat(wl_seat *seat, WaylandBackend *backend); + virtual ~WaylandSeat(); + + void installCursorImage(wl_buffer *image, const QSize &size, const QPoint &hotspot); + void installCursorImage(const QImage &image, const QPoint &hotspot); + void setInstallCursor(bool install); + bool isInstallCursor() const { + return m_installCursor; + } + + 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::Surface *m_cursor; + 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; + bool m_installCursor; +}; + +/** +* @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 surface and its shell mapping. +*/ +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); + virtual ~WaylandBackend(); + void init() override; + wl_display *display(); + KWayland::Client::Compositor *compositor(); + KWayland::Client::ShmPool *shmPool(); + + KWayland::Client::Surface *surface() const; + QSize shellSurfaceSize() const; + + Screens *createScreens(QObject *parent = nullptr) override; + OpenGLBackend *createOpenGLBackend() override; + QPainterBackend *createQPainterBackend() override; + + QSize screenSize() const override { + return shellSurfaceSize(); + } + + void flush(); + + void togglePointerConfinement(); + + QVector supportedCompositors() const override; + +Q_SIGNALS: + void shellSurfaceSizeChanged(const QSize &size); + void systemCompositorDied(); + void connectionFailed(); +private: + void initConnection(); + void createSurface(); + template + void setupSurface(T *surface); + void updateWindowTitle(); + wl_display *m_display; + KWayland::Client::EventQueue *m_eventQueue; + KWayland::Client::Registry *m_registry; + KWayland::Client::Compositor *m_compositor; + KWayland::Client::Shell *m_shell; + KWayland::Client::Surface *m_surface; + KWayland::Client::ShellSurface *m_shellSurface; + KWayland::Client::XdgShell *m_xdgShell = nullptr; + KWayland::Client::XdgShellSurface *m_xdgShellSurface = nullptr; + QScopedPointer m_seat; + KWayland::Client::ShmPool *m_shm; + KWayland::Client::ConnectionThread *m_connectionThreadObject; + KWayland::Client::PointerConstraints *m_pointerConstraints = nullptr; + KWayland::Client::ConfinedPointer *m_pointerConfinement = nullptr; + QThread *m_connectionThread; + bool m_isPointerConfined = false; +}; + +inline +wl_display *WaylandBackend::display() +{ + return m_display; +} + +inline +KWayland::Client::Compositor *WaylandBackend::compositor() +{ + return m_compositor; +} + +inline +KWayland::Client::ShmPool* WaylandBackend::shmPool() +{ + return m_shm; +} + +inline +KWayland::Client::Surface *WaylandBackend::surface() const +{ + return m_surface; +} + +} // namespace Wayland +} // namespace KWin + +#endif // KWIN_WAYLAND_BACKEND_H diff --git a/plugins/platforms/x11/CMakeLists.txt b/plugins/platforms/x11/CMakeLists.txt new file mode 100644 index 0000000..8addf0b --- /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..677f9f6 --- /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..1990fc4 --- /dev/null +++ b/plugins/platforms/x11/common/eglonxbackend.cpp @@ -0,0 +1,544 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2010, 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 + foreach (const QRect & r, damage.rects()) { + 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() +{ + 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()->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..0063971 --- /dev/null +++ b/plugins/platforms/x11/common/eglonxbackend.h @@ -0,0 +1,108 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~EglOnXBackend(); + virtual void screenGeometryChanged(const QSize &size); + virtual SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; + virtual QRegion prepareRenderingFrame(); + virtual void endRenderingFrame(const QRegion &damage, const QRegion &damagedRegion); + virtual OverlayWindow* overlayWindow() override; + virtual bool usesOverlayWindow() const override; + void init() override; + + bool isX11TextureFromPixmapSupported() const { + return m_x11TextureFromPixmapSupported; + } + +protected: + virtual void present(); + 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: + virtual ~EglTexture(); + virtual void onDamage(); + 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/standalone/CMakeLists.txt b/plugins/platforms/x11/standalone/CMakeLists.txt new file mode 100644 index 0000000..a1ec496 --- /dev/null +++ b/plugins/platforms/x11/standalone/CMakeLists.txt @@ -0,0 +1,43 @@ +set(X11PLATFORM_SOURCES + edge.cpp + logging.cpp + x11cursor.cpp + x11_platform.cpp + screens_xrandr.cpp + windowselector.cpp + overlaywindow_x11.cpp + screenedges_filter.cpp + non_composited_outline.cpp + x11_decoration_renderer.cpp + xfixes_cursor_event_filter.cpp + effects_x11.cpp + effects_mouse_interception_x11_filter.cpp + sync_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}) +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..700d086 --- /dev/null +++ b/plugins/platforms/x11/standalone/edge.cpp @@ -0,0 +1,144 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Arthur Arlt +Copyright (C) 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. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ +#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 = Cursor::self(); +#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(); + Cursor::self()->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..6934b7d --- /dev/null +++ b/plugins/platforms/x11/standalone/edge.h @@ -0,0 +1,79 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Arthur Arlt +Copyright (C) 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. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2009 Lucas Murray + +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, see . +*********************************************************************/ +#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); + virtual ~WindowBasedEdge(); + + 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: + virtual void doGeometryUpdate(); + virtual void doActivate() override; + virtual void doDeactivate() override; + virtual void doStartApproaching(); + virtual void doStopApproaching(); + virtual void doUpdateBlocking(); + +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..e33dedb --- /dev/null +++ b/plugins/platforms/x11/standalone/effects_mouse_interception_x11_filter.cpp @@ -0,0 +1,65 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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 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..740a87a --- /dev/null +++ b/plugins/platforms/x11/standalone/effects_mouse_interception_x11_filter.h @@ -0,0 +1,43 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..adb26de --- /dev/null +++ b/plugins/platforms/x11/standalone/effects_x11.cpp @@ -0,0 +1,114 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2010, 2011, 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#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() = default; + +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 = Cursor::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..738bf55 --- /dev/null +++ b/plugins/platforms/x11/standalone/effects_x11.h @@ -0,0 +1,58 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2010, 2011, 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~EffectsHandlerImplX11(); + + 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..be7845e --- /dev/null +++ b/plugins/platforms/x11/standalone/glx_context_attribute_builder.cpp @@ -0,0 +1,53 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..c600c8f --- /dev/null +++ b/plugins/platforms/x11/standalone/glx_context_attribute_builder.h @@ -0,0 +1,32 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..a2c570e --- /dev/null +++ b/plugins/platforms/x11/standalone/glxbackend.cpp @@ -0,0 +1,936 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2012 Martin Gräßlin + +Based on glcompmgr code by Felix Bellaby. +Using code from Compiz and Beryl. + +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, see . +*********************************************************************/ + +// 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 +// Qt +#include +#include +#include +// system +#include + +#include +#include +#if HAVE_DL_LIBRARY +#include +#endif +#include + +#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(NULL) + , glxWindow(None) + , ctx(nullptr) + , m_bufferAge(0) + , haveSwapInterval(false) + , m_x11Display(display) +{ +} + +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(); + + 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 = NULL; + } + + 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, 0, true, attribs.data()); + if (ctx) { + qCDebug(KWIN_X11STANDALONE) << "Created GLX context with attributes:" << &(*it); + break; + } + } + } + + if (!ctx) + ctx = glXCreateNewContext(display(), fbconfig, GLX_RGBA_TYPE, NULL, 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 = 0; + return false; + } + + 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, NULL); + 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 + }; + + // Try to find a double buffered configuration + int count = 0; + GLXFBConfig *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; + 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); + + qCDebug(KWIN_X11STANDALONE, "Choosing GLXFBConfig %#x X visual %#x depth %d RGBA %d:%d:%d:%d ZS %d:%d", + fbconfig_id, visual_id, visualDepth(visual_id), red, green, blue, alpha, depth, stencil); + } + + 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" << 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" << 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" << hex << fbc_id << " for visual 0x" << 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; + 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); + setSwapInterval(0); + result = 0; // hint proper behavior + qCWarning(KWIN_X11STANDALONE) << "\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'); + } + } else if (blocksForRetrace()) { + // at least the nvidia blob manages to swap async, ie. return immediately on double + // buffering - what messes our timing calculation and leads to laggy behavior #346275 + glXWaitGL(); + } + } else { + waitSync(); + glXSwapBuffers(display(), glxWindow); + } + if (supportsBufferAge()) { + glXQueryDrawable(display(), glxWindow, GLX_BACK_BUFFER_AGE_EXT, (GLuint *) &m_bufferAge); + } + } else if (m_haveMESACopySubBuffer) { + foreach (const QRect & r, lastDamage().rects()) { + // 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() +{ + 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, NULL); + } + 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 { + 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->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..ff3a0d8 --- /dev/null +++ b/plugins/platforms/x11/standalone/glxbackend.h @@ -0,0 +1,150 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~GlxBackend(); + virtual void screenGeometryChanged(const QSize &size); + virtual SceneOpenGLTexturePrivate *createBackendTexture(SceneOpenGLTexture *texture) override; + virtual QRegion prepareRenderingFrame(); + virtual void endRenderingFrame(const QRegion &damage, const QRegion &damagedRegion); + virtual bool makeCurrent() override; + virtual void doneCurrent() override; + virtual OverlayWindow* overlayWindow() override; + virtual bool usesOverlayWindow() const override; + void init() override; + +protected: + virtual void present(); + +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: + virtual ~GlxTexture(); + virtual void onDamage(); + virtual bool loadTexture(WindowPixmap *pixmap) override; + virtual OpenGLBackend *backend(); + +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..bad2ada --- /dev/null +++ b/plugins/platforms/x11/standalone/logging.cpp @@ -0,0 +1,21 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..399d4d2 --- /dev/null +++ b/plugins/platforms/x11/standalone/logging.h @@ -0,0 +1,26 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..dd03bba --- /dev/null +++ b/plugins/platforms/x11/standalone/non_composited_outline.cpp @@ -0,0 +1,150 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Arthur Arlt +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +// 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..1744ff9 --- /dev/null +++ b/plugins/platforms/x11/standalone/non_composited_outline.h @@ -0,0 +1,59 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Arthur Arlt + +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, see . +*********************************************************************/ +#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); + virtual ~NonCompositedOutlineVisual(); + virtual void show(); + virtual void hide(); + +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..5a78766 --- /dev/null +++ b/plugins/platforms/x11/standalone/overlaywindow_x11.cpp @@ -0,0 +1,213 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Arthur Arlt + +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, see . +*********************************************************************/ + +#include "overlaywindow_x11.h" + +#include "kwinglobals.h" +#include "composite.h" +#include "screens.h" +#include "utils.h" +#include "xcbutils.h" + +#include "assert.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() +{ + 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) +{ + assert(m_window != XCB_WINDOW_NONE); + 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, NULL); +} + +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() +{ + 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() +{ + 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; + QVector< QRect > rects = reg.rects(); + xcb_rectangle_t *xrects = new xcb_rectangle_t[rects.count()]; + for (int i = 0; + i < rects.count(); + ++i) { + xrects[ i ].x = rects[ i ].x(); + xrects[ i ].y = rects[ i ].y(); + xrects[ i ].width = rects[ i ].width(); + xrects[ i ].height = rects[ i ].height(); + } + xcb_shape_rectangles(connection(), XCB_SHAPE_SO_SET, XCB_SHAPE_SK_BOUNDING, XCB_CLIP_ORDERING_UNSORTED, + m_window, 0, 0, rects.count(), xrects); + delete[] xrects; + setupInputShape(m_window); + m_shape = reg; +} + +void OverlayWindowX11::resize(const QSize &size) +{ + 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..0ca5b14 --- /dev/null +++ b/plugins/platforms/x11/standalone/overlaywindow_x11.h @@ -0,0 +1,57 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2011 Arthur Arlt + +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, see . +*********************************************************************/ + +#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(); + /// 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..6958ecf --- /dev/null +++ b/plugins/platforms/x11/standalone/screenedges_filter.cpp @@ -0,0 +1,65 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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()), true); + } else { + ScreenEdges::self()->check(rootPos, QDateTime::fromMSecsSinceEpoch(mouseEvent->time)); + } + // 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)); + } + 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..19fb1c0 --- /dev/null +++ b/plugins/platforms/x11/standalone/screenedges_filter.h @@ -0,0 +1,37 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..4f55283 --- /dev/null +++ b/plugins/platforms/x11/standalone/screens_xrandr.cpp @@ -0,0 +1,230 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "screens_xrandr.h" +#ifndef KWIN_UNIT_TEST +#include "composite.h" +#include "options.h" +#include "workspace.h" +#endif +#include "xcbutils.h" + + +namespace KWin +{ + +XRandRScreens::XRandRScreens(QObject *parent) + : Screens(parent) + , X11EventFilter(Xcb::Extensions::self()->randrNotifyEvent()) +{ +} + +XRandRScreens::~XRandRScreens() = default; + +template +void XRandRScreens::update() +{ + auto fallback = [this]() { + m_geometries << QRect(); + m_refreshRates << -1.0f; + m_names << "Xinerama"; + setCount(1); + }; + m_geometries.clear(); + m_names.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()) { + m_geometries << geo; + m_refreshRates << refreshRate; + QString name; + for (int j = 0; j < info->num_outputs; ++j) { + Xcb::RandR::OutputInfo outputInfo(outputInfos.at(j)); + if (crtcs[i] == outputInfo->crtc) { + name = outputInfo.name(); + break; + } + } + m_names << name; + } + } + if (m_geometries.isEmpty()) { + fallback(); + return; + } + + setCount(m_geometries.count()); +} + + +void XRandRScreens::init() +{ + KWin::Screens::init(); + // we need to call ScreenResources at least once to be able to use current + update(); + emit changed(); +} + +QRect XRandRScreens::geometry(int screen) const +{ + if (screen >= m_geometries.size() || screen < 0) { + return QRect(); + } + return m_geometries.at(screen).isValid() ? m_geometries.at(screen) : + QRect(QPoint(0, 0), displaySize()); // xinerama, lacks RandR +} + +QString XRandRScreens::name(int screen) const +{ + if (screen >= m_names.size() || screen < 0) { + return QString(); + } + return m_names.at(screen); +} + +int XRandRScreens::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; +} + +float XRandRScreens::refreshRate(int screen) const +{ + if (screen >= m_refreshRates.size() || screen < 0) { + return -1.0f; + } + return m_refreshRates.at(screen); +} + +QSize XRandRScreens::size(int screen) const +{ + const QRect geo = geometry(screen); + if (!geo.isValid()) { + return QSize(); + } + return geo.size(); +} + +void XRandRScreens::updateCount() +{ + update(); +} + +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 = defaultScreen(); + 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; + } +#ifndef KWIN_UNIT_TEST + if (workspace()->compositing()) { + // desktopResized() should take care of when the size or + // shape of the desktop has changed, but we also want to + // catch refresh rate changes + if (Compositor::self()->xrrRefreshRate() != Options::currentRefreshRate()) + Compositor::self()->setCompositeResetTimer(0); + } +#endif + + return false; +} + +QSize XRandRScreens::displaySize() const +{ + xcb_screen_t *screen = defaultScreen(); + 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..5a7a748 --- /dev/null +++ b/plugins/platforms/x11/standalone/screens_xrandr.h @@ -0,0 +1,61 @@ +/******************************************************************** +KWin - the KDE window manager +This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_SCREENS_XRANDR_H +#define KWIN_SCREENS_XRANDR_H +// kwin +#include "screens.h" +#include "x11eventfilter.h" +// Qt +#include + +namespace KWin +{ + +class XRandRScreens : public Screens, public X11EventFilter +{ + Q_OBJECT +public: + XRandRScreens(QObject *parent); + virtual ~XRandRScreens(); + void init() override; + QRect geometry(int screen) const override; + QString name(int screen) const override; + int number(const QPoint& pos) const override; + float refreshRate(int screen) const override; + QSize size(int screen) const override; + QSize displaySize() const override; + + using QObject::event; + bool event(xcb_generic_event_t *event) override; + +protected Q_SLOTS: + void updateCount() override; + +private: + template + void update(); + QVector m_geometries; + QVector m_refreshRates; + QVector m_names; +}; + +} // namespace + +#endif diff --git a/plugins/platforms/x11/standalone/sync_filter.cpp b/plugins/platforms/x11/standalone/sync_filter.cpp new file mode 100644 index 0000000..7f2506c --- /dev/null +++ b/plugins/platforms/x11/standalone/sync_filter.cpp @@ -0,0 +1,48 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#include "sync_filter.h" +#include "client.h" +#include "workspace.h" +#include "xcbutils.h" + +namespace KWin +{ + +SyncFilter::SyncFilter() + : X11EventFilter(QVector{Xcb::Extensions::self()->syncAlarmNotifyEvent()}) +{ +} + +bool SyncFilter::event(xcb_generic_event_t *event) +{ + auto e = reinterpret_cast< xcb_sync_alarm_notify_event_t* >(event); + auto client = workspace()->findClient( + [e] (const Client *c) { + const auto syncRequest = c->getSyncRequest(); + return e->alarm == syncRequest.alarm && e->counter_value.hi == syncRequest.value.hi && e->counter_value.lo == syncRequest.value.lo; + } + ); + if (client) { + client->handleSync(); + } + return false; +} + +} diff --git a/plugins/platforms/x11/standalone/sync_filter.h b/plugins/platforms/x11/standalone/sync_filter.h new file mode 100644 index 0000000..5549b3f --- /dev/null +++ b/plugins/platforms/x11/standalone/sync_filter.h @@ -0,0 +1,38 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#ifndef KWIN_SYNC_FILTER_H +#define KWIN_SYNC_FILTER_H +#include "x11eventfilter.h" + +namespace KWin +{ +class X11Cursor; + +class SyncFilter : public X11EventFilter +{ +public: + explicit SyncFilter(); + + bool event(xcb_generic_event_t *event) override; +}; + +} + +#endif diff --git a/plugins/platforms/x11/standalone/windowselector.cpp b/plugins/platforms/x11/standalone/windowselector.cpp new file mode 100644 index 0000000..4f7d39e --- /dev/null +++ b/plugins/platforms/x11/standalone/windowselector.cpp @@ -0,0 +1,273 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "windowselector.h" +#include "client.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), NULL)); + 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 Cursor::x11Cursor(Qt::CrossCursor); + } + xcb_cursor_t cursor = Cursor::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(Cursor::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; + } + Cursor::setPos(Cursor::pos() + QPoint(mx, my)); + if (returnPressed) { + if (m_callback) { + selectWindowUnderPointer(); + } else if (m_pointSelectionFallback) { + m_pointSelectionFallback(Cursor::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; + Client* client = NULL; + 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..710acd8 --- /dev/null +++ b/plugins/platforms/x11/standalone/windowselector.h @@ -0,0 +1,70 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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(); + + 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..26cbc43 --- /dev/null +++ b/plugins/platforms/x11/standalone/x11.json @@ -0,0 +1,76 @@ +{ + "KPlugin": { + "Description": "Platform plugin for standalone x11 in kwin_x11.", + "Description[ca@valencia]": "Connector de plataforma per una X11 autònoma en el kwin_x11.", + "Description[ca]": "Connector de plataforma per 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[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[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[nl]": "x11-alleenstaand", + "Name[nn]": "X11 frittståande", + "Name[pl]": "x11-wolnostojący", + "Name[pt]": "x11-autónomo", + "Name[pt_BR]": "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..d8b4954 --- /dev/null +++ b/plugins/platforms/x11/standalone/x11_decoration_renderer.cpp @@ -0,0 +1,109 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "x11_decoration_renderer.h" +#include "decorations/decoratedclient.h" +#include "client.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.isNull()) { + 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.byteCount(), 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..bbcda97 --- /dev/null +++ b/plugins/platforms/x11/standalone/x11_decoration_renderer.h @@ -0,0 +1,55 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~X11Renderer(); + + 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_platform.cpp b/plugins/platforms/x11/standalone/x11_platform.cpp new file mode 100644 index 0000000..e7b5fba --- /dev/null +++ b/plugins/platforms/x11/standalone/x11_platform.cpp @@ -0,0 +1,442 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "x11_platform.h" +#include "x11cursor.h" +#include "edge.h" +#include "sync_filter.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 + +#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 + connect(kwinApp(), &Application::workspaceCreated, this, + [this] { + if (Xcb::Extensions::self()->isSyncAvailable()) { + m_syncFilter = std::make_unique(); + } + } + ); +} + +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(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(); +} + +/* + Updates xTime(). This used to simply fetch current timestamp from the server, + but that can cause xTime() to be newer than timestamp of events that are + still in our events queue, thus e.g. making XSetInputFocus() caused by such + event to be ignored. Therefore events queue is searched for first + event with timestamp, and extra PropertyNotify is generated in order to make + sure such event is found. +*/ +void X11StandalonePlatform::updateXTime() +{ + // NOTE: QX11Info::getTimestamp does not yet search the event queue as the old + // solution did. This means there might be regressions currently. See the + // documentation above on how it should be done properly. + kwinApp()->setX11Time(QX11Info::getTimestamp(), Application::TimestampUpdate::Always); +} + +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; +} + +} diff --git a/plugins/platforms/x11/standalone/x11_platform.h b/plugins/platforms/x11/standalone/x11_platform.h new file mode 100644 index 0000000..7321775 --- /dev/null +++ b/plugins/platforms/x11/standalone/x11_platform.h @@ -0,0 +1,102 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_X11_PLATFORM_H +#define KWIN_X11_PLATFORM_H +#include "platform.h" + +#include + +#include + +#include + +namespace KWin +{ +class SyncFilter; +class XInputIntegration; +class WindowSelector; +class X11EventFilter; + +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); + virtual ~X11StandalonePlatform(); + 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; + + void updateXTime() 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; + +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(); + + XInputIntegration *m_xinputIntegration = nullptr; + QThread *m_openGLFreezeProtectionThread = nullptr; + QTimer *m_openGLFreezeProtection = nullptr; + Display *m_x11Display; + QScopedPointer m_windowSelector; + QScopedPointer m_screenEdgesFilter; + std::unique_ptr m_syncFilter; + +}; + +} + +#endif diff --git a/plugins/platforms/x11/standalone/x11cursor.cpp b/plugins/platforms/x11/standalone/x11cursor.cpp new file mode 100644 index 0000000..711d2a0 --- /dev/null +++ b/plugins/platforms/x11/standalone/x11cursor.cpp @@ -0,0 +1,196 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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) +{ + 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(connection(), defaultScreen(), &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..85aa899 --- /dev/null +++ b/plugins/platforms/x11/standalone/x11cursor.h @@ -0,0 +1,85 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~X11Cursor(); + + void schedulePoll() { + m_needsPoll = true; + } + + /** + * @internal + * + * Called from X11 event handler. + */ + void notifyCursorChanged(); + +protected: + virtual xcb_cursor_t getX11Cursor(CursorShape shape); + xcb_cursor_t getX11Cursor(const QByteArray &name) override; + virtual void doSetPos(); + virtual void doGetPos(); + virtual void doStartMousePolling(); + virtual void doStopMousePolling(); + virtual void doStartCursorTracking(); + virtual void doStopCursorTracking(); + +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..b23bafe --- /dev/null +++ b/plugins/platforms/x11/standalone/xfixes_cursor_event_filter.cpp @@ -0,0 +1,40 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..cb15efe --- /dev/null +++ b/plugins/platforms/x11/standalone/xfixes_cursor_event_filter.h @@ -0,0 +1,41 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Flöser + +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, see . +*********************************************************************/ +#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..3225070 --- /dev/null +++ b/plugins/platforms/x11/standalone/xinputintegration.cpp @@ -0,0 +1,312 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "xinputintegration.h" +#include "main.h" +#include "logging.h" +#include "gestures.h" +#include "platform.h" +#include "screenedge.h" +#include "x11cursor.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 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; +}; + +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}) + {} + virtual ~XInputEventFilter() = 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() = 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..f98e7f3 --- /dev/null +++ b/plugins/platforms/x11/standalone/xinputintegration.h @@ -0,0 +1,69 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~XInputIntegration(); + + 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..8236c6b --- /dev/null +++ b/plugins/platforms/x11/windowed/CMakeLists.txt @@ -0,0 +1,17 @@ +set(X11BACKEND_SOURCES + logging.cpp + egl_x11_backend.cpp + scene_qpainter_x11_backend.cpp + x11windowed_backend.cpp +) + +include_directories(${CMAKE_SOURCE_DIR}/platformsupport/scenes/opengl) +add_library(KWinWaylandX11Backend MODULE ${X11BACKEND_SOURCES}) +target_link_libraries(KWinWaylandX11Backend eglx11common kwin kwinxrenderutils X11::XCB SceneQPainterBackend SceneOpenGLBackend) + +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..c9e649a --- /dev/null +++ b/plugins/platforms/x11/windowed/egl_x11_backend.cpp @@ -0,0 +1,121 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..5234801 --- /dev/null +++ b/plugins/platforms/x11/windowed/egl_x11_backend.h @@ -0,0 +1,57 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~EglX11Backend(); + virtual QRegion prepareRenderingFrame(); + virtual void endRenderingFrame(const QRegion &damage, const QRegion &damagedRegion); + virtual 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: + virtual void present(); + 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..07fe50d --- /dev/null +++ b/plugins/platforms/x11/windowed/logging.cpp @@ -0,0 +1,21 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..6a316c1 --- /dev/null +++ b/plugins/platforms/x11/windowed/logging.h @@ -0,0 +1,26 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..200cc6a --- /dev/null +++ b/plugins/platforms/x11/windowed/scene_qpainter_x11_backend.cpp @@ -0,0 +1,104 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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.byteCount(), 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..1e57a3d --- /dev/null +++ b/plugins/platforms/x11/windowed/scene_qpainter_x11_backend.h @@ -0,0 +1,65 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~X11WindowedQPainterBackend(); + + 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..611f0e7 --- /dev/null +++ b/plugins/platforms/x11/windowed/x11.json @@ -0,0 +1,78 @@ +{ + "KPlugin": { + "Description": "Render to a nested window on X11 windowing system.", + "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 jendela tersarang pada sistem perjendelaan X11", + "Description[it]": "Resa in una finestra nidificata su sistema di finestre X11.", + "Description[ko]": "X11 ì°½ 시스템에서 실행 중인 창에 렌더링합니다.", + "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[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]": "x", + "Name[id]": "x11", + "Name[it]": "x11", + "Name[ko]": "x11", + "Name[nl]": "x11", + "Name[nn]": "x11", + "Name[pl]": "x11", + "Name[pt]": "X11", + "Name[pt_BR]": "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..2e26a00 --- /dev/null +++ b/plugins/platforms/x11/windowed/x11windowed_backend.cpp @@ -0,0 +1,495 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "x11windowed_backend.h" +#include "scene_qpainter_x11_backend.h" +#include "logging.h" +#include "wayland_server.h" +#include "xcbutils.h" +#include "egl_x11_backend.h" +#include "screens.h" +#include +// KDE +#include +#include +#include +#include +#include +#include +// kwayland +#include +#include +#include +#include +// xcb +#include +// 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); + } + for (auto it = m_windows.begin(); it != m_windows.end(); ++it) { + xcb_unmap_window(m_connection, (*it).window); + xcb_destroy_window(m_connection, (*it).window); + delete (*it).winInfo; + } + 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; + } + } + XRenderUtils::init(m_connection, m_screen->root); + createWindow(); + connect(kwinApp(), &Application::workspaceCreated, this, &X11WindowedBackend::startEventReading); + connect(this, &X11WindowedBackend::cursorChanged, this, + [this] { + createCursor(softwareCursor(), softwareCursorHotspot()); + } + ); + setReady(true); + waylandServer()->seat()->setHasPointer(true); + waylandServer()->seat()->setHasKeyboard(true); + emit screensQueried(); + } else { + emit initFailed(); + } +} + +void X11WindowedBackend::createWindow() +{ + Xcb::Atom protocolsAtom(QByteArrayLiteral("WM_PROTOCOLS"), false, m_connection); + Xcb::Atom deleteWindowAtom(QByteArrayLiteral("WM_DELETE_WINDOW"), false, m_connection); + for (int i = 0; i < initialOutputCount(); ++i) { + Output o; + o.window = xcb_generate_id(m_connection); + uint32_t mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; + const uint32_t values[] = { + m_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 + }; + o.scale = initialOutputScale(); + o.size = initialWindowSize() * o.scale; + if (!m_windows.isEmpty()) { + const auto &p = m_windows.last(); + o.internalPosition = QPoint(p.internalPosition.x() + p.size.width() / p.scale, 0); + } + xcb_create_window(m_connection, XCB_COPY_FROM_PARENT, o.window, m_screen->root, + 0, 0, o.size.width(), o.size.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, mask, values); + + o.winInfo = new NETWinInfo(m_connection, o.window, m_screen->root, NET::WMWindowType, NET::Properties2()); + o.winInfo->setWindowType(NET::Normal); + o.winInfo->setPid(QCoreApplication::applicationPid()); + QIcon windowIcon = QIcon::fromTheme(QStringLiteral("kwin")); + auto addIcon = [&o, &windowIcon] (const QSize &size) { + if (windowIcon.actualSize(size) != size) { + return; + } + NETIcon icon; + icon.data = windowIcon.pixmap(size).toImage().bits(); + icon.size.width = size.width(); + icon.size.height = size.height(); + o.winInfo->setIcon(icon, false); + }; + addIcon(QSize(16, 16)); + addIcon(QSize(32, 32)); + addIcon(QSize(48, 48)); + + xcb_map_window(m_connection, o.window); + + m_protocols = protocolsAtom; + m_deleteWindowProtocol = deleteWindowAtom; + xcb_change_property(m_connection, XCB_PROP_MODE_REPLACE, o.window, m_protocols, XCB_ATOM_ATOM, 32, 1, &m_deleteWindowProtocol); + + m_windows << o; + } + + 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); +} + +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); + auto it = std::find_if(m_windows.constBegin(), m_windows.constEnd(), [event] (const Output &o) { return o.window == event->event; }); + if (it == m_windows.constEnd()) { + break; + } + //generally we don't need to normalise input to the output scale; however because we're getting input + //from a host window that doesn't understand scaling, we need to apply it ourselves so the cursor matches + pointerMotion(QPointF(event->root_x - (*it).xPosition.x() + (*it).internalPosition.x(), + event->root_y - (*it).xPosition.y() + (*it).internalPosition.y()) / it->scale, + 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); + auto it = std::find_if(m_windows.constBegin(), m_windows.constEnd(), [event] (const Output &o) { return o.window == event->event; }); + if (it == m_windows.constEnd()) { + break; + } + pointerMotion(QPointF(event->root_x - (*it).xPosition.x() + (*it).internalPosition.x(), + event->root_y - (*it).xPosition.y() + (*it).internalPosition.y()) / it->scale, + 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; + 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_windows.constBegin(); it != m_windows.constEnd(); ++it) { + (*it).winInfo->setName(title.toUtf8().constData()); + } +} + +void X11WindowedBackend::handleClientMessage(xcb_client_message_event_t *event) +{ + auto it = std::find_if(m_windows.begin(), m_windows.end(), [event] (const Output &o) { return o.window == event->window; }); + if (it == m_windows.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_windows.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 o = *it; + it = m_windows.erase(it); + xcb_unmap_window(m_connection, o.window); + xcb_destroy_window(m_connection, o.window); + delete o.winInfo; + + // update the sizes + int x = o.internalPosition.x(); + for (; it != m_windows.end(); ++it) { + (*it).internalPosition.setX(x); + x += (*it).size.width(); + } + QMetaObject::invokeMethod(screens(), "updateCount"); + } + } + } +} + +void X11WindowedBackend::handleButtonPress(xcb_button_press_event_t *event) +{ + auto it = std::find_if(m_windows.constBegin(), m_windows.constEnd(), [event] (const Output &o) { return o.window == event->event; }); + if (it == m_windows.constEnd()) { + 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); + } else { + pointerAxisVertical(delta * s_defaultAxisStepDistance, event->time); + } + 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; + } + + pointerMotion(QPointF(event->root_x - (*it).xPosition.x() + (*it).internalPosition.x(), + event->root_y - (*it).xPosition.y() + (*it).internalPosition.y()) / it->scale, + 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) +{ + auto it = std::find_if(m_windows.begin(), m_windows.end(), [event] (const Output &o) { return o.window == event->window; }); + if (it == m_windows.end()) { + return; + } + (*it).xPosition = QPoint(event->x, event->y); + QSize s = QSize(event->width, event->height); + if (s != (*it).size) { + (*it).size = s; + int x = (*it).internalPosition.x() + (*it).size.width() / (*it).scale; + it++; + for (; it != m_windows.end(); ++it) { + (*it).internalPosition.setX(x); + x += (*it).size.width() / (*it).scale; + } + 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.byteCount(), 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_windows.constBegin(); it != m_windows.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); + markCursorAsRendered(); +} + +xcb_window_t X11WindowedBackend::rootWindow() const +{ + if (!m_screen) { + return XCB_WINDOW_NONE; + } + return m_screen->root; +} + +Screens *X11WindowedBackend::createScreens(QObject *parent) +{ + return new BasicScreens(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_windows.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_windows.count()) { + return XCB_WINDOW_NONE; + } + return m_windows.at(screen).window; +} + +QVector X11WindowedBackend::screenGeometries() const +{ + QVector ret; + for (auto it = m_windows.constBegin(); it != m_windows.constEnd(); ++it) { + ret << QRect((*it).internalPosition, (*it).size / (*it).scale); + } + return ret; +} + +QVector X11WindowedBackend::screenScales() const +{ + QVector ret; + for (auto it = m_windows.constBegin(); it != m_windows.constEnd(); ++it) { + ret << (*it).scale; + } + return ret; +} + +} diff --git a/plugins/platforms/x11/windowed/x11windowed_backend.h b/plugins/platforms/x11/windowed/x11windowed_backend.h new file mode 100644 index 0000000..e17149a --- /dev/null +++ b/plugins/platforms/x11/windowed/x11windowed_backend.h @@ -0,0 +1,113 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 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); + virtual ~X11WindowedBackend(); + void init() override; + QVector screenGeometries() const override; + QVector screenScales() const override; + + xcb_connection_t *connection() const { + return m_connection; + } + 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; + + Screens *createScreens(QObject *parent = nullptr) override; + OpenGLBackend *createOpenGLBackend() override; + QPainterBackend* createQPainterBackend() override; + void warpPointer(const QPointF &globalPos) override; + + QVector supportedCompositors() const override { + return QVector{OpenGLCompositing, QPainterCompositing}; + } + +Q_SIGNALS: + void sizeChanged(); + +private: + void createWindow(); + 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); + + xcb_connection_t *m_connection = nullptr; + xcb_screen_t *m_screen = nullptr; + xcb_key_symbols_t *m_keySymbols = nullptr; + int m_screenNumber = 0; + struct Output { + xcb_window_t window = XCB_WINDOW_NONE; + QSize size; + qreal scale = 1; + QPoint xPosition; + QPoint internalPosition; + NETWinInfo *winInfo = nullptr; + }; + QVector m_windows; + 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; +}; + +} + +#endif diff --git a/plugins/qpa/CMakeLists.txt b/plugins/qpa/CMakeLists.txt new file mode 100644 index 0000000..4094ae7 --- /dev/null +++ b/plugins/qpa/CMakeLists.txt @@ -0,0 +1,50 @@ +include_directories(${Qt5Core_PRIVATE_INCLUDE_DIRS}) +include_directories(${Qt5Gui_PRIVATE_INCLUDE_DIRS}) + +set(QPA_SOURCES + abstractplatformcontext.cpp + backingstore.cpp + integration.cpp + main.cpp + nativeinterface.cpp + platformcontextwayland.cpp + platformcursor.cpp + screen.cpp + sharingplatformcontext.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}) + +if(Qt5Core_VERSION VERSION_LESS "5.8.0") + add_definitions(-DQ_FONTCONFIGDATABASE) + set(QT5PLATFORMSUPPORT_LIBS Qt5PlatformSupport::Qt5PlatformSupport) +else() + set(QT5PLATFORMSUPPORT_LIBS + Qt5FontDatabaseSupport::Qt5FontDatabaseSupport + Qt5ThemeSupport::Qt5ThemeSupport + Qt5EventDispatcherSupport::Qt5EventDispatcherSupport +) +endif() + +target_link_libraries(KWinQpaPlugin + kwin + KF5::WaylandClient + ${QT5PLATFORMSUPPORT_LIBS} + ${FONTCONFIG_LIBRARIES} + ${FREETYPE_LIBRARIES} +) + +if(HAVE_WAYLAND_EGL) + target_link_libraries(KWinQpaPlugin Wayland::Egl) +endif() + +install( + TARGETS + KWinQpaPlugin + DESTINATION + ${PLUGIN_INSTALL_DIR}/platforms/ +) diff --git a/plugins/qpa/abstractplatformcontext.cpp b/plugins/qpa/abstractplatformcontext.cpp new file mode 100644 index 0000000..507a151 --- /dev/null +++ b/plugins/qpa/abstractplatformcontext.cpp @@ -0,0 +1,257 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "abstractplatformcontext.h" +#include "integration.h" +#include "egl_context_attribute_builder.h" +#include + +#include + +namespace KWin +{ + +namespace QPA +{ + +static bool isOpenGLES() +{ + if (qstrcmp(qgetenv("KWIN_COMPOSE"), "O2ES") == 0) { + return true; + } + return QOpenGLContext::openGLModuleType() == QOpenGLContext::LibGLES; +} + +static EGLConfig configFromGLFormat(EGLDisplay dpy, const QSurfaceFormat &format) +{ +#define SIZE( __buffer__ ) format.__buffer__##BufferSize() > 0 ? format.__buffer__##BufferSize() : 0 + // not setting samples as QtQuick doesn't need it + const EGLint config_attribs[] = { + EGL_SURFACE_TYPE, EGL_WINDOW_BIT, + EGL_RED_SIZE, SIZE(red), + EGL_GREEN_SIZE, SIZE(green), + EGL_BLUE_SIZE, SIZE(blue), + EGL_ALPHA_SIZE, SIZE(alpha), + EGL_DEPTH_SIZE, SIZE(depth), + EGL_STENCIL_SIZE, SIZE(stencil), + EGL_RENDERABLE_TYPE, isOpenGLES() ? EGL_OPENGL_ES2_BIT : EGL_OPENGL_BIT, + EGL_NONE, + }; + qCDebug(KWIN_QPA) << "Trying to find a format with: rgba/depth/stencil" << (SIZE(red)) << (SIZE(green)) <<( SIZE(blue)) << (SIZE(alpha)) << (SIZE(depth)) << (SIZE(stencil)); +#undef SIZE + + EGLint count; + EGLConfig configs[1024]; + if (eglChooseConfig(dpy, config_attribs, configs, 1, &count) == EGL_FALSE) { + qCWarning(KWIN_QPA) << "eglChooseConfig failed"; + return 0; + } + if (count != 1) { + qCWarning(KWIN_QPA) << "eglChooseConfig did not return any configs"; + return 0; + } + return configs[0]; +} + +static QSurfaceFormat formatFromConfig(EGLDisplay dpy, EGLConfig config) +{ + QSurfaceFormat format; + EGLint value = 0; +#define HELPER(__egl__, __qt__) \ + eglGetConfigAttrib(dpy, config, EGL_##__egl__, &value); \ + format.set##__qt__(value); \ + value = 0; + +#define BUFFER_HELPER(__eglColor__, __color__) \ + HELPER(__eglColor__##_SIZE, __color__##BufferSize) + + BUFFER_HELPER(RED, Red) + BUFFER_HELPER(GREEN, Green) + BUFFER_HELPER(BLUE, Blue) + BUFFER_HELPER(ALPHA, Alpha) + BUFFER_HELPER(STENCIL, Stencil) + BUFFER_HELPER(DEPTH, Depth) +#undef BUFFER_HELPER + HELPER(SAMPLES, Samples) +#undef HELPER + format.setRenderableType(isOpenGLES() ? QSurfaceFormat::OpenGLES : QSurfaceFormat::OpenGL); + format.setStereo(false); + + return format; +} + +AbstractPlatformContext::AbstractPlatformContext(QOpenGLContext *context, EGLDisplay display, EGLConfig config) + : QPlatformOpenGLContext() + , m_eglDisplay(display) + , m_config(config ? config :configFromGLFormat(m_eglDisplay, context->format())) + , m_format(formatFromConfig(m_eglDisplay, m_config)) +{ +} + +AbstractPlatformContext::~AbstractPlatformContext() +{ + if (m_context != EGL_NO_CONTEXT) { + eglDestroyContext(m_eglDisplay, m_context); + } +} + +void AbstractPlatformContext::doneCurrent() +{ + eglMakeCurrent(m_eglDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT); +} + +QSurfaceFormat AbstractPlatformContext::format() const +{ + return m_format; +} + +QFunctionPointer AbstractPlatformContext::getProcAddress(const char *procName) +{ + return eglGetProcAddress(procName); +} + +bool AbstractPlatformContext::isValid() const +{ + return m_context != EGL_NO_CONTEXT; +} + +bool AbstractPlatformContext::bindApi() +{ + if (eglBindAPI(isOpenGLES() ? EGL_OPENGL_ES_API : EGL_OPENGL_API) == EGL_FALSE) { + qCWarning(KWIN_QPA) << "eglBindAPI failed"; + return false; + } + return true; +} + +void AbstractPlatformContext::createContext(EGLContext shareContext) +{ + 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(), 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; +} + +} +} diff --git a/plugins/qpa/abstractplatformcontext.h b/plugins/qpa/abstractplatformcontext.h new file mode 100644 index 0000000..984679a --- /dev/null +++ b/plugins/qpa/abstractplatformcontext.h @@ -0,0 +1,68 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_QPA_ABSTRACTPLATFORMCONTEXT_H +#define KWIN_QPA_ABSTRACTPLATFORMCONTEXT_H + +#include +#include "fixqopengl.h" +#include +#include + +namespace KWin +{ +namespace QPA +{ +class Integration; + +class AbstractPlatformContext : public QPlatformOpenGLContext +{ +public: + explicit AbstractPlatformContext(QOpenGLContext *context, EGLDisplay display, EGLConfig config = nullptr); + virtual ~AbstractPlatformContext(); + + void doneCurrent() override; + QSurfaceFormat format() const override; + bool isValid() const override; + QFunctionPointer getProcAddress(const char *procName) override; + +protected: + EGLDisplay eglDisplay() const { + return m_eglDisplay; + } + EGLConfig config() const { + return m_config; + } + bool bindApi(); + EGLContext eglContext() const { + return m_context; + } + void createContext(EGLContext shareContext = EGL_NO_CONTEXT); + +private: + EGLDisplay m_eglDisplay; + EGLConfig m_config; + EGLContext m_context = EGL_NO_CONTEXT; + QSurfaceFormat m_format; +}; + +} +} + +#endif diff --git a/plugins/qpa/backingstore.cpp b/plugins/qpa/backingstore.cpp new file mode 100644 index 0000000..2ffe084 --- /dev/null +++ b/plugins/qpa/backingstore.cpp @@ -0,0 +1,119 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "window.h" +#include "backingstore.h" +#include "../../wayland_server.h" + +#include +#include +#include +#include + +namespace KWin +{ +namespace QPA +{ + +BackingStore::BackingStore(QWindow *w, KWayland::Client::ShmPool *shm) + : QPlatformBackingStore(w) + , m_shm(shm) + , m_backBuffer(QSize(), QImage::Format_ARGB32_Premultiplied) +{ + QObject::connect(m_shm, &KWayland::Client::ShmPool::poolResized, + [this] { + 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_ARGB32_Premultiplied); + } + ); +} + +BackingStore::~BackingStore() = default; + +QPaintDevice *BackingStore::paintDevice() +{ + return &m_backBuffer; +} + +void BackingStore::resize(const QSize &size, const QRegion &staticContents) +{ + Q_UNUSED(staticContents) + m_size = size; + if (!m_buffer) { + return; + } + m_buffer.toStrongRef()->setUsed(false); + m_buffer.clear(); +} + +void BackingStore::flush(QWindow *window, const QRegion ®ion, const QPoint &offset) +{ + Q_UNUSED(region) + Q_UNUSED(offset) + auto s = static_cast(window->handle())->surface(); + if (!s) { + return; + } + s->attachBuffer(m_buffer); + // TODO: proper damage region + s->damage(QRect(QPoint(0, 0), m_backBuffer.size())); + s->commit(KWayland::Client::Surface::CommitFlag::None); + waylandServer()->internalClientConection()->flush(); + waylandServer()->dispatch(); +} + +void BackingStore::beginPaint(const QRegion&) +{ + 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); + } + } + auto oldBuffer = m_buffer.toStrongRef(); + m_buffer.clear(); + m_buffer = m_shm->getBuffer(m_size, m_size.width() * 4); + if (!m_buffer) { + m_backBuffer = QImage(); + return; + } + auto b = m_buffer.toStrongRef(); + b->setUsed(true); + m_backBuffer = QImage(b->address(), m_size.width(), m_size.height(), QImage::Format_ARGB32_Premultiplied); + if (oldBuffer) { + b->copy(oldBuffer->address()); + } else { + m_backBuffer.fill(Qt::transparent); + } +} + +} +} diff --git a/plugins/qpa/backingstore.h b/plugins/qpa/backingstore.h new file mode 100644 index 0000000..0e00de6 --- /dev/null +++ b/plugins/qpa/backingstore.h @@ -0,0 +1,60 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_QPA_BACKINGSTORE_H +#define KWIN_QPA_BACKINGSTORE_H + +#include + +namespace KWayland +{ +namespace Client +{ +class Buffer; +class ShmPool; +} +} + +namespace KWin +{ +namespace QPA +{ + +class BackingStore : public QPlatformBackingStore +{ +public: + explicit BackingStore(QWindow *w, KWayland::Client::ShmPool *shm); + virtual ~BackingStore(); + + QPaintDevice *paintDevice() override; + void flush(QWindow *window, const QRegion ®ion, const QPoint &offset) override; + void resize(const QSize &size, const QRegion &staticContents) override; + void beginPaint(const QRegion &) override; + +private: + KWayland::Client::ShmPool *m_shm; + QWeakPointer m_buffer; + QImage m_backBuffer; + QSize m_size; +}; + +} +} + +#endif diff --git a/plugins/qpa/integration.cpp b/plugins/qpa/integration.cpp new file mode 100644 index 0000000..d934b97 --- /dev/null +++ b/plugins/qpa/integration.cpp @@ -0,0 +1,294 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#define WL_EGL_PLATFORM 1 +#include "integration.h" +#include "platform.h" +#include "backingstore.h" +#include "nativeinterface.h" +#include "platformcontextwayland.h" +#include "screen.h" +#include "sharingplatformcontext.h" +#include "window.h" +#include "../../virtualkeyboard.h" +#include "../../main.h" +#include "../../screens.h" +#include "../../wayland_server.h" + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace KWin +{ + +namespace QPA +{ + +Integration::Integration() + : QObject() + , QPlatformIntegration() + , m_fontDb(new QGenericUnixFontDatabase()) + , m_nativeInterface(new NativeInterface(this)) + , m_inputContext() +{ +} + +Integration::~Integration() = default; + +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); + screenAdded(dummyScreen); + m_screens << dummyScreen; + m_inputContext.reset(QPlatformInputContextFactory::create(QStringLiteral("qtvirtualkeyboard"))); + qunsetenv("QT_IM_MODULE"); + if (!m_inputContext.isNull()) { + connect(qApp, &QGuiApplication::focusObjectChanged, this, + [this] { + if (VirtualKeyboard::self() && qApp->focusObject() != VirtualKeyboard::self()) { + m_inputContext->setFocusObject(VirtualKeyboard::self()); + } + } + ); + connect(kwinApp(), &Application::workspaceCreated, this, + [this] { + if (VirtualKeyboard::self()) { + m_inputContext->setFocusObject(VirtualKeyboard::self()); + } + } + ); + connect(qApp->inputMethod(), &QInputMethod::visibleChanged, this, + [this] { + if (qApp->inputMethod()->isVisible()) { + if (QWindow *w = VirtualKeyboard::self()->inputPanel()) { + QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); + } + } + } + ); + } +} + +QAbstractEventDispatcher *Integration::createEventDispatcher() const +{ + return new QUnixEventDispatcherQPA; +} + +QPlatformBackingStore *Integration::createPlatformBackingStore(QWindow *window) const +{ + auto registry = waylandServer()->internalClientRegistry(); + const auto shm = registry->interface(KWayland::Client::Registry::Interface::Shm); + if (shm.name == 0u) { + return nullptr; + } + return new BackingStore(window, registry->createShmPool(shm.name, shm.version, window)); +} + +QPlatformWindow *Integration::createPlatformWindow(QWindow *window) const +{ + auto c = compositor(); + auto s = shell(); + if (!s || !c) { + return new QPlatformWindow(window); + } else { + // don't set window as parent, cause infinite recursion in PlasmaQuick::Dialog + auto surface = c->createSurface(c); + return new Window(window, surface, s->createSurface(surface, surface), this); + } +} + +QPlatformFontDatabase *Integration::fontDatabase() const +{ + return m_fontDb; +} + +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)}); +} + +QPlatformNativeInterface *Integration::nativeInterface() const +{ + return m_nativeInterface; +} + +QPlatformOpenGLContext *Integration::createPlatformOpenGLContext(QOpenGLContext *context) const +{ + if (kwinApp()->platform()->supportsQpaContext()) { + return new SharingPlatformContext(context); + } + if (kwinApp()->platform()->sceneEglDisplay() != EGL_NO_DISPLAY) { + auto s = kwinApp()->platform()->sceneEglSurface(); + if (s != EGL_NO_SURFACE) { + // try a SharingPlatformContext with a created surface + return new SharingPlatformContext(context, s, kwinApp()->platform()->sceneEglConfig()); + } + } + if (m_eglDisplay == EGL_NO_DISPLAY) { + const_cast(this)->initEgl(); + } + if (m_eglDisplay == EGL_NO_DISPLAY) { + return nullptr; + } + return new PlatformContextWayland(context, const_cast(this)); +} + +void Integration::initScreens() +{ + QVector newScreens; + newScreens.reserve(qMax(screens()->count(), 1)); + for (int i = 0; i < screens()->count(); i++) { + auto screen = new Screen(i); + screenAdded(screen); + newScreens << screen; + } + if (newScreens.isEmpty()) { + auto dummyScreen = new Screen(-1); + screenAdded(dummyScreen); + newScreens << dummyScreen; + } + while (!m_screens.isEmpty()) { + destroyScreen(m_screens.takeLast()); + } + m_screens = newScreens; +} + +KWayland::Client::Compositor *Integration::compositor() const +{ + if (!m_compositor) { + using namespace KWayland::Client; + auto registry = waylandServer()->internalClientRegistry(); + const auto c = registry->interface(Registry::Interface::Compositor); + if (c.name != 0u) { + const_cast(this)->m_compositor = registry->createCompositor(c.name, c.version, registry); + } + } + return m_compositor; +} + +KWayland::Client::Shell *Integration::shell() const +{ + if (!m_shell) { + using namespace KWayland::Client; + auto registry = waylandServer()->internalClientRegistry(); + const auto s = registry->interface(Registry::Interface::Shell); + if (s.name != 0u) { + const_cast(this)->m_shell = registry->createShell(s.name, s.version, registry); + } + } + return m_shell; +} + +EGLDisplay Integration::eglDisplay() const +{ + return m_eglDisplay; +} + +void Integration::initEgl() +{ + Q_ASSERT(m_eglDisplay == EGL_NO_DISPLAY); + // This variant uses Wayland as the EGL platform + qputenv("EGL_PLATFORM", "wayland"); + m_eglDisplay = eglGetDisplay(waylandServer()->internalClientConection()->display()); + if (m_eglDisplay == EGL_NO_DISPLAY) { + return; + } + // call eglInitialize in a thread to not block + QFuture future = QtConcurrent::run([this] () -> bool { + EGLint major, minor; + if (eglInitialize(m_eglDisplay, &major, &minor) == EGL_FALSE) { + return false; + } + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + return false; + } + return true; + }); + // TODO: make this better + while (!future.isFinished()) { + waylandServer()->internalClientConection()->flush(); + QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents); + } + if (!future.result()) { + eglTerminate(m_eglDisplay); + m_eglDisplay = EGL_NO_DISPLAY; + } +} + +QPlatformInputContext *Integration::inputContext() const +{ + return m_inputContext.data(); +} + +} +} diff --git a/plugins/qpa/integration.h b/plugins/qpa/integration.h new file mode 100644 index 0000000..d8bf6bc --- /dev/null +++ b/plugins/qpa/integration.h @@ -0,0 +1,88 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_QPA_INTEGRATION_H +#define KWIN_QPA_INTEGRATION_H + +#include +#include "fixqopengl.h" + +#include +#include +#include + +namespace KWayland +{ +namespace Client +{ +class Registry; +class Compositor; +class Shell; +} +} + +namespace KWin +{ +namespace QPA +{ + +class Screen; + +class Integration : public QObject, public QPlatformIntegration +{ + Q_OBJECT +public: + explicit Integration(); + virtual ~Integration(); + + bool hasCapability(Capability cap) const override; + QPlatformWindow *createPlatformWindow(QWindow *window) 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; + QPlatformNativeInterface *nativeInterface() const override; + QPlatformOpenGLContext *createPlatformOpenGLContext(QOpenGLContext *context) const override; + + void initialize() override; + QPlatformInputContext *inputContext() const override; + + KWayland::Client::Compositor *compositor() const; + EGLDisplay eglDisplay() const; + +private: + void initScreens(); + void initEgl(); + KWayland::Client::Shell *shell() const; + + QPlatformFontDatabase *m_fontDb; + QPlatformNativeInterface *m_nativeInterface; + KWayland::Client::Compositor *m_compositor = nullptr; + KWayland::Client::Shell *m_shell = nullptr; + EGLDisplay m_eglDisplay = EGL_NO_DISPLAY; + Screen *m_dummyScreen = nullptr; + QScopedPointer m_inputContext; + 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..d13e582 --- /dev/null +++ b/plugins/qpa/main.cpp @@ -0,0 +1,48 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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(QLatin1Literal("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/nativeinterface.cpp b/plugins/qpa/nativeinterface.cpp new file mode 100644 index 0000000..4b1e0df --- /dev/null +++ b/plugins/qpa/nativeinterface.cpp @@ -0,0 +1,106 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "nativeinterface.h" +#include "integration.h" +#include "window.h" +#include "../../wayland_server.h" + +#include + +#include +#include +#include + +namespace KWin +{ +namespace QPA +{ + +static const QByteArray s_displayKey = QByteArrayLiteral("display"); +static const QByteArray s_wlDisplayKey = QByteArrayLiteral("wl_display"); +static const QByteArray s_compositorKey = QByteArrayLiteral("compositor"); +static const QByteArray s_surfaceKey = QByteArrayLiteral("surface"); + +NativeInterface::NativeInterface(Integration *integration) + : QPlatformNativeInterface() + , m_integration(integration) +{ +} + +void *NativeInterface::nativeResourceForIntegration(const QByteArray &resource) +{ + const QByteArray r = resource.toLower(); + if (r == s_displayKey || r == s_wlDisplayKey) { + if (!waylandServer() || !waylandServer()->internalClientConection()) { + return nullptr; + } + return waylandServer()->internalClientConection()->display(); + } + if (r == s_compositorKey) { + return static_cast(*m_integration->compositor()); + } + return nullptr; +} + +void *NativeInterface::nativeResourceForWindow(const QByteArray &resource, QWindow *window) +{ + const QByteArray r = resource.toLower(); + if (r == s_displayKey || r == s_wlDisplayKey) { + if (!waylandServer() || !waylandServer()->internalClientConection()) { + return nullptr; + } + return waylandServer()->internalClientConection()->display(); + } + if (r == s_compositorKey) { + return static_cast(*m_integration->compositor()); + } + if (r == s_surfaceKey && window) { + if (auto handle = window->handle()) { + if (auto surface = static_cast(handle)->surface()) { + return static_cast(*surface); + } + } + } + return nullptr; +} + +static void roundtrip() +{ + if (!waylandServer()) { + return; + } + auto c = waylandServer()->internalClientConection(); + if (!c) { + return; + } + c->flush(); + waylandServer()->dispatch(); +} + +QFunctionPointer NativeInterface::platformFunction(const QByteArray &function) const +{ + if (qstrcmp(function.toLower(), "roundtrip") == 0) { + return &roundtrip; + } + return nullptr; +} + +} +} diff --git a/plugins/qpa/nativeinterface.h b/plugins/qpa/nativeinterface.h new file mode 100644 index 0000000..4a9f8e3 --- /dev/null +++ b/plugins/qpa/nativeinterface.h @@ -0,0 +1,47 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_QPA_NATIVEINTERFACE_H +#define KWIN_QPA_NATIVEINTERFACE_H + +#include + +namespace KWin +{ +namespace QPA +{ + +class Integration; + +class NativeInterface : public QPlatformNativeInterface +{ +public: + explicit NativeInterface(Integration *integration); + void *nativeResourceForIntegration(const QByteArray &resource) override; + void *nativeResourceForWindow(const QByteArray &resourceString, QWindow *window) override; + QFunctionPointer platformFunction(const QByteArray &function) const override; + +private: + Integration *m_integration; +}; + +} +} + +#endif diff --git a/plugins/qpa/platformcontextwayland.cpp b/plugins/qpa/platformcontextwayland.cpp new file mode 100644 index 0000000..f43ae9f --- /dev/null +++ b/plugins/qpa/platformcontextwayland.cpp @@ -0,0 +1,77 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "platformcontextwayland.h" +#include "integration.h" +#include "window.h" + +namespace KWin +{ + +namespace QPA +{ + +PlatformContextWayland::PlatformContextWayland(QOpenGLContext *context, Integration *integration) + : AbstractPlatformContext(context, integration, integration->eglDisplay()) +{ + create(); +} + +bool PlatformContextWayland::makeCurrent(QPlatformSurface *surface) +{ + Window *window = static_cast(surface); + EGLSurface s = window->eglSurface(); + if (s == EGL_NO_SURFACE) { + window->createEglSurface(eglDisplay(), config()); + s = window->eglSurface(); + if (s == EGL_NO_SURFACE) { + return false; + } + } + return eglMakeCurrent(eglDisplay(), s, s, eglContext()); +} + +bool PlatformContextWayland::isSharing() const +{ + return false; +} + +void PlatformContextWayland::swapBuffers(QPlatformSurface *surface) +{ + Window *window = static_cast(surface); + EGLSurface s = window->eglSurface(); + if (s == EGL_NO_SURFACE) { + return; + } + eglSwapBuffers(eglDisplay(), s); +} + +void PlatformContextWayland::create() +{ + if (config() == 0) { + return; + } + if (!bindApi()) { + return; + } + createContext(); +} + +} +} diff --git a/plugins/qpa/platformcontextwayland.h b/plugins/qpa/platformcontextwayland.h new file mode 100644 index 0000000..fbf7c21 --- /dev/null +++ b/plugins/qpa/platformcontextwayland.h @@ -0,0 +1,49 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_QPA_PLATFORMCONTEXTWAYLAND_H +#define KWIN_QPA_PLATFORMCONTEXTWAYLAND_H + +#include "abstractplatformcontext.h" + +namespace KWin +{ +namespace QPA +{ +class Integration; + +class PlatformContextWayland : public AbstractPlatformContext +{ +public: + explicit PlatformContextWayland(QOpenGLContext *context, Integration *integration); + + void swapBuffers(QPlatformSurface *surface) override; + + bool makeCurrent(QPlatformSurface *surface) override; + + bool isSharing() const override; + +private: + void create(); +}; + +} +} + +#endif diff --git a/plugins/qpa/platformcursor.cpp b/plugins/qpa/platformcursor.cpp new file mode 100644 index 0000000..d563165 --- /dev/null +++ b/plugins/qpa/platformcursor.cpp @@ -0,0 +1,53 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "platformcursor.h" +#include "../../cursor.h" + +namespace KWin +{ +namespace QPA +{ + +PlatformCursor::PlatformCursor() + : QPlatformCursor() +{ +} + +PlatformCursor::~PlatformCursor() = default; + +QPoint PlatformCursor::pos() const +{ + return Cursor::pos(); +} + +void PlatformCursor::setPos(const QPoint &pos) +{ + Cursor::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..e694d4e --- /dev/null +++ b/plugins/qpa/platformcursor.h @@ -0,0 +1,43 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_QPA_PLATFORMCURSOR_H +#define KWIN_QPA_PLATFORMCURSOR_H + +#include + +namespace KWin +{ +namespace QPA +{ + +class PlatformCursor : public QPlatformCursor +{ +public: + PlatformCursor(); + virtual ~PlatformCursor(); + 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..5eae7d9 --- /dev/null +++ b/plugins/qpa/screen.cpp @@ -0,0 +1,80 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "screen.h" +#include "platformcursor.h" +#include "screens.h" +#include "wayland_server.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 force_dpi = qEnvironmentVariableIsSet("QT_WAYLAND_FORCE_DPI") ? qEnvironmentVariableIntValue("QT_WAYLAND_FORCE_DPI") : -1; + if (force_dpi > 0) { + return QDpi(force_dpi, force_dpi); + } + + return QPlatformScreen::logicalDpi(); +} + +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..2753a4e --- /dev/null +++ b/plugins/qpa/screen.h @@ -0,0 +1,54 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~Screen(); + + 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/sharingplatformcontext.cpp b/plugins/qpa/sharingplatformcontext.cpp new file mode 100644 index 0000000..a6e4b2b --- /dev/null +++ b/plugins/qpa/sharingplatformcontext.cpp @@ -0,0 +1,114 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "sharingplatformcontext.h" +#include "integration.h" +#include "window.h" +#include "../../platform.h" +#include "../../wayland_server.h" +#include "../../shell_client.h" +#include + +#include +#include + +namespace KWin +{ + +namespace QPA +{ + +SharingPlatformContext::SharingPlatformContext(QOpenGLContext *context) + : SharingPlatformContext(context, EGL_NO_SURFACE) +{ +} + +SharingPlatformContext::SharingPlatformContext(QOpenGLContext *context, const EGLSurface &surface, EGLConfig config) + : AbstractPlatformContext(context, kwinApp()->platform()->sceneEglDisplay(), config) + , m_surface(surface) +{ + create(); +} + +bool SharingPlatformContext::makeCurrent(QPlatformSurface *surface) +{ + Window *window = static_cast(surface); + + // QOpenGLContext::makeCurrent in Qt5.12 calls platfrom->setContext before setCurrentContext + // but binding the content FBO looks up the format from the current context, so we need // to make sure sure Qt knows what the correct one is already + QOpenGLContextPrivate::setCurrentContext(context()); + if (eglMakeCurrent(eglDisplay(), m_surface, m_surface, eglContext())) { + window->bindContentFBO(); + return true; + } + qCWarning(KWIN_QPA) << "Failed to make context current"; + EGLint error = eglGetError(); + if (error != EGL_SUCCESS) { + qCWarning(KWIN_QPA) << "EGL error code: " << error; + } + + return false; +} + +bool SharingPlatformContext::isSharing() const +{ + return false; +} + +void SharingPlatformContext::swapBuffers(QPlatformSurface *surface) +{ + Window *window = static_cast(surface); + auto c = window->shellClient(); + if (!c) { + qCDebug(KWIN_QPA) << "SwapBuffers called but there is no ShellClient"; + return; + } + context()->makeCurrent(surface->surface()); + glFlush(); + c->setInternalFramebufferObject(window->swapFBO()); + window->bindContentFBO(); +} + +GLuint SharingPlatformContext::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 SharingPlatformContext::create() +{ + if (config() == 0) { + qCWarning(KWIN_QPA) << "Did not get an EGL config"; + return; + } + if (!bindApi()) { + qCWarning(KWIN_QPA) << "Could not bind API."; + return; + } + createContext(kwinApp()->platform()->sceneEglContext()); +} + +} +} diff --git a/plugins/qpa/sharingplatformcontext.h b/plugins/qpa/sharingplatformcontext.h new file mode 100644 index 0000000..1a0f72c --- /dev/null +++ b/plugins/qpa/sharingplatformcontext.h @@ -0,0 +1,54 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_QPA_SHARINGPLATFORMCONTEXT_H +#define KWIN_QPA_SHARINGPLATFORMCONTEXT_H + +#include "abstractplatformcontext.h" + +namespace KWin +{ +namespace QPA +{ +class Integration; + +class SharingPlatformContext : public AbstractPlatformContext +{ +public: + explicit SharingPlatformContext(QOpenGLContext *context); + explicit SharingPlatformContext(QOpenGLContext *context, const EGLSurface &surface, EGLConfig config = nullptr); + + void swapBuffers(QPlatformSurface *surface) override; + + GLuint defaultFramebufferObject(QPlatformSurface *surface) const override; + + bool makeCurrent(QPlatformSurface *surface) override; + + bool isSharing() const override; + +private: + void create(); + + EGLSurface m_surface; +}; + +} +} + +#endif diff --git a/plugins/qpa/window.cpp b/plugins/qpa/window.cpp new file mode 100644 index 0000000..12431d3 --- /dev/null +++ b/plugins/qpa/window.cpp @@ -0,0 +1,177 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#define WL_EGL_PLATFORM 1 +#include "integration.h" +#include "window.h" +#include "../../shell_client.h" +#include "../../wayland_server.h" +#include + +#include +#include + +#include +#include +#include +#include + +namespace KWin +{ +namespace QPA +{ +static quint32 s_windowId = 0; + +Window::Window(QWindow *window, KWayland::Client::Surface *surface, KWayland::Client::ShellSurface *shellSurface, const Integration *integration) + : QPlatformWindow(window) + , m_surface(surface) + , m_shellSurface(shellSurface) + , m_windowId(++s_windowId) + , m_integration(integration) +{ + QObject::connect(m_surface, &QObject::destroyed, window, [this] { m_surface = nullptr;}); + QObject::connect(m_shellSurface, &QObject::destroyed, window, [this] { m_shellSurface = nullptr;}); + waylandServer()->internalClientConection()->flush(); +} + +Window::~Window() +{ + unmap(); + if (m_eglSurface != EGL_NO_SURFACE) { + eglDestroySurface(m_integration->eglDisplay(), m_eglSurface); + } +#if HAVE_WAYLAND_EGL + if (m_eglWaylandWindow) { + wl_egl_window_destroy(m_eglWaylandWindow); + } +#endif + delete m_shellSurface; + delete m_surface; +} + +WId Window::winId() const +{ + return m_windowId; +} + +void Window::setVisible(bool visible) +{ + if (!visible) { + unmap(); + } + QPlatformWindow::setVisible(visible); +} + +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()); + } + if (m_contentFBO) { + if (m_contentFBO->width() != geometry().width() || m_contentFBO->height() != geometry().height()) { + m_resized = true; + } + } +#if HAVE_WAYLAND_EGL + if (m_eglWaylandWindow) { + wl_egl_window_resize(m_eglWaylandWindow, geometry().width(), geometry().height(), 0, 0); + } +#endif + QWindowSystemInterface::handleGeometryChange(window(), geometry()); +} + +void Window::unmap() +{ + if (m_shellClient) { + m_shellClient->setInternalFramebufferObject(QSharedPointer()); + } + if (m_surface) { + m_surface->attachBuffer(KWayland::Client::Buffer::Ptr()); + m_surface->commit(KWayland::Client::Surface::CommitFlag::None); + } + if (waylandServer()->internalClientConection()) { + waylandServer()->internalClientConection()->flush(); + } +} + +void Window::createEglSurface(EGLDisplay dpy, EGLConfig config) +{ +#if HAVE_WAYLAND_EGL + const QSize size = window()->size(); + m_eglWaylandWindow = wl_egl_window_create(*m_surface, size.width(), size.height()); + if (!m_eglWaylandWindow) { + return; + } + m_eglSurface = eglCreateWindowSurface(dpy, config, m_eglWaylandWindow, nullptr); +#else + Q_UNUSED(dpy) + Q_UNUSED(config) +#endif +} + +void Window::bindContentFBO() +{ + if (m_resized || !m_contentFBO) { + createFBO(); + } + m_contentFBO->bind(); +} + +QSharedPointer Window::swapFBO() +{ + auto fbo = m_contentFBO; + m_contentFBO.clear(); + return fbo; +} + +void Window::createFBO() +{ + const QRect &r = geometry(); + if (m_contentFBO && r.size().isEmpty()) { + return; + } + m_contentFBO.reset(new QOpenGLFramebufferObject(r.width(), r.height(), QOpenGLFramebufferObject::CombinedDepthStencil)); + if (!m_contentFBO->isValid()) { + qCWarning(KWIN_QPA) << "Content FBO is not valid"; + } + m_resized = false; +} + +ShellClient *Window::shellClient() +{ + if (!m_shellClient) { + waylandServer()->dispatch(); + m_shellClient = waylandServer()->findClient(window()); + } + return m_shellClient; +} + +} +} diff --git a/plugins/qpa/window.h b/plugins/qpa/window.h new file mode 100644 index 0000000..e8caedc --- /dev/null +++ b/plugins/qpa/window.h @@ -0,0 +1,104 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_QPA_WINDOW_H +#define KWIN_QPA_WINDOW_H + +#include +#include "fixqopengl.h" + +#include +#include +// wayland +#include +#if HAVE_WAYLAND_EGL +#include +#endif + +class QOpenGLFramebufferObject; + +#if HAVE_WAYLAND_EGL +struct wl_egl_window; +#endif + +namespace KWayland +{ +namespace Client +{ +class Surface; +class ShellSurface; +} +} + +namespace KWin +{ + +class ShellClient; + +namespace QPA +{ + +class Integration; + +class Window : public QPlatformWindow +{ +public: + explicit Window(QWindow *window, KWayland::Client::Surface *surface, KWayland::Client::ShellSurface *shellSurface, const Integration *integration); + virtual ~Window(); + + void setVisible(bool visible) override; + void setGeometry(const QRect &rect) override; + WId winId() const override; + + KWayland::Client::Surface *surface() const { + return m_surface; + } + EGLSurface eglSurface() const { + return m_eglSurface; + } + void createEglSurface(EGLDisplay dpy, EGLConfig config); + + void bindContentFBO(); + const QSharedPointer &contentFBO() const { + return m_contentFBO; + } + QSharedPointer swapFBO(); + ShellClient *shellClient(); + +private: + void unmap(); + void createFBO(); + + KWayland::Client::Surface *m_surface; + KWayland::Client::ShellSurface *m_shellSurface; + EGLSurface m_eglSurface = EGL_NO_SURFACE; + QSharedPointer m_contentFBO; + bool m_resized = false; + ShellClient *m_shellClient = nullptr; +#if HAVE_WAYLAND_EGL + wl_egl_window *m_eglWaylandWindow = nullptr; +#endif + quint32 m_windowId; + const Integration *m_integration; +}; + +} +} + +#endif diff --git a/plugins/scenes/CMakeLists.txt b/plugins/scenes/CMakeLists.txt new file mode 100644 index 0000000..eb84a43 --- /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..a01a265 --- /dev/null +++ b/plugins/scenes/opengl/CMakeLists.txt @@ -0,0 +1,27 @@ +set(SCENE_OPENGL_SRCS 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 +) + +add_library(KWinSceneOpenGL MODULE scene_opengl.cpp) +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/opengl.json b/plugins/scenes/opengl/opengl.json new file mode 100644 index 0000000..eca72bd --- /dev/null +++ b/plugins/scenes/opengl/opengl.json @@ -0,0 +1,71 @@ +{ + "CompositingType": 1, + "KPlugin": { + "Description": "KWin Compositor plugin rendering through OpenGL", + "Description[ca@valencia]": "Connector del Compositor del KWin que renderitza a través de l'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[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[id]": "Plugin KWin Compositor perenderan melalui OpenGL", + "Description[it]": "Estensione del compositore di KWin per la resa tramite OpenGL", + "Description[ko]": "OpenGL로 렌더링하는 KWin 컴포지터 플러그인", + "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[ca@valencia]": "SceneOpenGL", + "Name[ca]": "SceneOpenGL", + "Name[cs]": "SceneOpenGL", + "Name[da]": "SceneOpenGL", + "Name[de]": "SceneOpenGL", + "Name[en_GB]": "SceneOpenGL", + "Name[es]": "SceneOpenGL", + "Name[eu]": "SceneOpenGL", + "Name[fi]": "SceneOpenGL", + "Name[fr]": "SceneOpenGL", + "Name[gl]": "SceneOpenGL", + "Name[id]": "SceneOpenGL", + "Name[it]": "SceneOpenGL", + "Name[ko]": "SceneOpenGL", + "Name[nl]": "SceneOpenGL", + "Name[nn]": "SceneOpenGL", + "Name[pl]": "OpenGL sceny", + "Name[pt]": "SceneOpenGL", + "Name[pt_BR]": "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/scene_opengl.cpp b/plugins/scenes/opengl/scene_opengl.cpp new file mode 100644 index 0000000..dc33b02 --- /dev/null +++ b/plugins/scenes/opengl/scene_opengl.cpp @@ -0,0 +1,2610 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2009, 2010, 2011 Martin Gräßlin + +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 , + +Copyright © 2011 NVIDIA Corporation + +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, see . +*********************************************************************/ +#include "scene_opengl.h" + +#include "platform.h" +#include "wayland_server.h" +#include "platformsupport/scenes/opengl/texture.h" + +#include + +#include "utils.h" +#include "client.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() +{ + 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. + 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() +{ + 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() +{ + 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; + } + if (!glPlatform->isGLES() && !m_backend->isSurfaceLessContext()) { + glDrawBuffer(GL_BACK); + } + + 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"; + } + } +} + +static SceneOpenGL *gs_debuggedScene = nullptr; +SceneOpenGL::~SceneOpenGL() +{ + // do cleanup after initBuffer() + gs_debuggedScene = nullptr; + SceneOpenGL::EffectFrame::cleanup(); + if (init_ok) { + delete m_syncManager; + + // backend might be still needed for a different scene + delete m_backend; + } +} + +static void scheduleVboReInit() +{ + if (!gs_debuggedScene) + return; + + static QPointer timer; + if (!timer) { + delete timer; + timer = new QTimer(gs_debuggedScene); + timer->setSingleShot(true); + QObject::connect(timer.data(), &QTimer::timeout, gs_debuggedScene, []() { + GLVertexBuffer::cleanup(); + GLVertexBuffer::initStatic(); + }); + } + timer->start(250); +} + +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; + } + } + + gs_debuggedScene = this; + + // 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 (message[length] == '\n' || message[length] == '\r') + --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: + // at least the nvidia driver seems prone to end up with invalid VBOs after + // transferring them between system heap and VRAM + // so we re-init them whenever this happens (typically when switching VT, resuming + // from STR and XRandR events - #344326 + if (strstr(message, "Buffer detailed info:") && strstr(message, "has been updated")) + scheduleVboReInit(); + // fall through! for general message printing + Q_FALLTHROUGH(); + 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); + +#ifndef NDEBUG + // 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 NULL; + } + SceneOpenGL *scene = NULL; + // first let's try an OpenGL 2 scene + if (SceneOpenGL2::supported(backend)) { + scene = new SceneOpenGL2(backend, parent); + if (scene->initFailed()) { + delete scene; + scene = NULL; + } 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 http://community.kde.org/KWin/Environment_Variables#KWIN_COMPOSE"; + } + delete backend; + } + + return scene; +} + +OverlayWindow *SceneOpenGL::overlayWindow() +{ + 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() +{ + // don't paint if we use hardware cursor + if (!kwinApp()->platform()->usesSoftwareCursor()) { + 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 = kwinApp()->platform()->softwareCursor(); + if (img.isNull()) { + return; + } + m_cursorTexture.reset(new GLTexture(img)); + }; + + // init now + updateCursorTexture(); + + // handle shape update on case cursor image changed + connect(Cursor::self(), &Cursor::cursorChanged, this, updateCursorTexture); + } + + // get cursor position in projection coordinates + const QPoint cursorPos = Cursor::pos() - kwinApp()->platform()->softwareCursorHotspot(); + const QRect cursorRect(0, 0, m_cursorTexture->width(), m_cursorTexture->height()); + 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(QRegion(cursorRect), cursorRect); + m_cursorTexture->unbind(); + + kwinApp()->platform()->markCursorAsRendered(); + + glDisable(GL_BLEND); +} + +qint64 SceneOpenGL::paint(QRegion damage, ToplevelList 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); + 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(screens()->scale(i)); + GLRenderTarget::setVirtualScreenScale(screens()->scale(i)); + + 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); // call generic implementation + paintCursor(); + + 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(QRegion region) +{ + 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 { + GLint limit[2]; + glGetIntegerv(GL_MAX_VIEWPORT_DIMS, limit); + if (limit[0] < size.width() || limit[1] < size.height()) { + QMetaObject::invokeMethod(Compositor::self(), "suspend", + Qt::QueuedConnection, Q_ARG(Compositor::SuspendReason, Compositor::AllReasonSuspend)); + const QString message = i18n("

OpenGL desktop effects not possible

" + "Your system cannot perform OpenGL Desktop Effects at the " + "current resolution

" + "You can try to select the XRender backend, but it " + "might be very slow for this resolution as well.
" + "Alternatively, lower the combined resolution of all screens " + "to %1x%2 ", limit[0], limit[1]); + const QString details = i18n("The demanded resolution exceeds the GL_MAX_VIEWPORT_DIMS " + "limitation of your GPU and is therefore not compatible " + "with the OpenGL compositor.
" + "XRender does not know such limitation, but the performance " + "will usually be impacted by the hardware limitations that " + "restrict the OpenGL viewport size."); + const int oldTimeout = QDBusConnection::sessionBus().interface()->timeout(); + QDBusConnection::sessionBus().interface()->setTimeout(500); + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.kwinCompositingDialog")).value()) { + QDBusInterface dialog( QStringLiteral("org.kde.kwinCompositingDialog"), QStringLiteral("/CompositorSettings"), QStringLiteral("org.kde.kwinCompositingDialog") ); + dialog.asyncCall(QStringLiteral("warn"), message, details, QString()); + } else { + const QString args = QLatin1String("warn ") + QString::fromUtf8(message.toLocal8Bit().toBase64()) + QLatin1String(" details ") + QString::fromUtf8(details.toLocal8Bit().toBase64()); + KProcess::startDetached(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("kwincompositing") << QStringLiteral("--args") << args); + } + QDBusConnection::sessionBus().interface()->setTimeout(oldTimeout); + return false; + } + glGetIntegerv(GL_MAX_TEXTURE_SIZE, limit); + if (limit[0] < size.width() || limit[0] < size.height()) { + KConfig cfg(QStringLiteral("kwin_dialogsrc")); + + if (!KConfigGroup(&cfg, "Notification Messages").readEntry("max_tex_warning", true)) + return true; + + const QString message = i18n("

OpenGL desktop effects might be unusable

" + "OpenGL Desktop Effects at the current resolution are supported " + "but might be exceptionally slow.
" + "Also large windows will turn entirely black.

" + "Consider to suspend compositing, switch to the XRender backend " + "or lower the resolution to %1x%1." , limit[0]); + const QString details = i18n("The demanded resolution exceeds the GL_MAX_TEXTURE_SIZE " + "limitation of your GPU, thus windows of that size cannot be " + "assigned to textures and will be entirely black.
" + "Also this limit will often be a performance level barrier despite " + "below GL_MAX_VIEWPORT_DIMS, because the driver might fall back to " + "software rendering in this case."); + const int oldTimeout = QDBusConnection::sessionBus().interface()->timeout(); + QDBusConnection::sessionBus().interface()->setTimeout(500); + if (QDBusConnection::sessionBus().interface()->isServiceRegistered(QStringLiteral("org.kde.kwinCompositingDialog")).value()) { + QDBusInterface dialog( QStringLiteral("org.kde.kwinCompositingDialog"), QStringLiteral("/CompositorSettings"), QStringLiteral("org.kde.kwinCompositingDialog") ); + dialog.asyncCall(QStringLiteral("warn"), message, details, QStringLiteral("kwin_dialogsrc:max_tex_warning")); + } else { + const QString args = QLatin1String("warn ") + QString::fromUtf8(message.toLocal8Bit().toBase64()) + QLatin1String(" details ") + + QString::fromUtf8(details.toLocal8Bit().toBase64()) + QLatin1String(" dontagain kwin_dialogsrc:max_tex_warning"); + KProcess::startDetached(QStringLiteral("kcmshell5"), QStringList() << QStringLiteral("kwincompositing") << QStringLiteral("--args") << args); + } + QDBusConnection::sessionBus().interface()->setTimeout(oldTimeout); + } + 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); +} + +bool SceneOpenGL::makeOpenGLContextCurrent() +{ + return m_backend->makeCurrent(); +} + +void SceneOpenGL::doneOpenGLContextCurrent() +{ + m_backend->doneCurrent(); +} + +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(); +} + +//**************************************** +// 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(NULL) +{ + 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() +{ +} + +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, QRegion region) +{ + m_screenProjectionMatrix = m_projectionMatrix; + + Scene::paintSimpleScreen(mask, region); +} + +void SceneOpenGL2::paintGenericScreen(int mask, 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(), NULL); + + ShaderBinder binder(ShaderTrait::UniformColor); + binder.shader()->setUniform(GLShader::ModelViewProjectionMatrix, m_projectionMatrix); + + vbo->render(GL_TRIANGLES); +} + +Scene::Window *SceneOpenGL2::createWindow(Toplevel *t) +{ + SceneOpenGL2Window *w = new SceneOpenGL2Window(t); + w->setScene(this); + return w; +} + +void SceneOpenGL2::finalDrawWindow(EffectWindowImpl* w, int mask, QRegion region, 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, QRegion region, 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 = NULL; + }); + } + m_lanczosFilter->performPaint(w, mask, region, data); + } else + w->sceneWindow()->performPaint(mask, region, data); +} + +//**************************************** +// SceneOpenGL::Window +//**************************************** + +SceneOpenGL::Window::Window(Toplevel* c) + : Scene::Window(c) + , m_scene(NULL) +{ +} + +SceneOpenGL::Window::~Window() +{ +} + +static SceneOpenGLTexture *s_frameTexture = NULL; +// Bind the window pixmap to an OpenGL texture. +bool SceneOpenGL::Window::bindTexture() +{ + s_frameTexture = NULL; + OpenGLWindowPixmap *pixmap = windowPixmap(); + if (!pixmap) { + return false; + } + s_frameTexture = pixmap->texture(); + if (pixmap->isDiscarded()) { + return !pixmap->texture()->isNull(); + } + + if (!window()->damage().isEmpty()) + m_scene->insertWait(); + + return pixmap->bind(); +} + +QMatrix4x4 SceneOpenGL::Window::transformation(int mask, const WindowPaintData &data) const +{ + QMatrix4x4 matrix; + matrix.translate(x(), y()); + + if (!(mask & 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 SceneOpenGL::Window::beginRenderWindow(int mask, const QRegion ®ion, WindowPaintData &data) +{ + if (region.isEmpty()) + return false; + + m_hardwareClipping = region != infiniteRegion() && (mask & PAINT_WINDOW_TRANSFORMED) && !(mask & 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 + foreach (const WindowQuad &quad, 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() || !s_frameTexture) { + return false; + } + + if (m_hardwareClipping) { + glEnable(GL_SCISSOR_TEST); + } + + // Update the texture filter + if (options->glSmoothScale() != 0 && + (mask & (PAINT_WINDOW_TRANSFORMED | PAINT_SCREEN_TRANSFORMED))) + filter = ImageFilterGood; + else + filter = ImageFilterFast; + + s_frameTexture->setFilter(filter == ImageFilterGood ? GL_LINEAR : GL_NEAREST); + + 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 SceneOpenGL::Window::endRenderWindow() +{ + if (m_hardwareClipping) { + glDisable(GL_SCISSOR_TEST); + } +} + +GLTexture *SceneOpenGL::Window::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* SceneOpenGL::Window::createWindowPixmap() +{ + return new OpenGLWindowPixmap(this, m_scene); +} + +//*************************************** +// SceneOpenGL2Window +//*************************************** +SceneOpenGL2Window::SceneOpenGL2Window(Toplevel *c) + : SceneOpenGL::Window(c) + , m_blendingEnabled(false) +{ +} + +SceneOpenGL2Window::~SceneOpenGL2Window() +{ +} + +QVector4D SceneOpenGL2Window::modulate(float opacity, float brightness) const +{ + const float a = opacity; + const float rgb = opacity * brightness; + + return QVector4D(rgb, rgb, rgb, a); +} + +void SceneOpenGL2Window::setBlendEnabled(bool enabled) +{ + if (enabled && !m_blendingEnabled) + glEnable(GL_BLEND); + else if (!enabled && m_blendingEnabled) + glDisable(GL_BLEND); + + m_blendingEnabled = enabled; +} + +void SceneOpenGL2Window::setupLeafNodes(LeafNode *nodes, const WindowQuadList *quads, const WindowPaintData &data) +{ + if (!quads[ShadowLeaf].isEmpty()) { + nodes[ShadowLeaf].texture = static_cast(m_shadow)->shadowTexture(); + nodes[ShadowLeaf].opacity = data.opacity(); + nodes[ShadowLeaf].hasAlpha = true; + nodes[ShadowLeaf].coordinateType = NormalizedCoordinates; + } + + if (!quads[DecorationLeaf].isEmpty()) { + nodes[DecorationLeaf].texture = getDecorationTexture(); + nodes[DecorationLeaf].opacity = data.opacity(); + nodes[DecorationLeaf].hasAlpha = true; + nodes[DecorationLeaf].coordinateType = UnnormalizedCoordinates; + } + + nodes[ContentLeaf].texture = s_frameTexture; + nodes[ContentLeaf].hasAlpha = !isOpaque(); + // TODO: ARGB crsoofading is atm. a hack, playing on opacities for two dumb SrcOver operations + // Should be a shader + if (data.crossFadeProgress() != 1.0 && (data.opacity() < 0.95 || toplevel->hasAlpha())) { + const float opacity = 1.0 - data.crossFadeProgress(); + nodes[ContentLeaf].opacity = data.opacity() * (1 - pow(opacity, 1.0f + 2.0f * data.opacity())); + } else { + nodes[ContentLeaf].opacity = data.opacity(); + } + nodes[ContentLeaf].coordinateType = UnnormalizedCoordinates; + + if (data.crossFadeProgress() != 1.0) { + OpenGLWindowPixmap *previous = previousWindowPixmap(); + nodes[PreviousContentLeaf].texture = previous ? previous->texture() : NULL; + nodes[PreviousContentLeaf].hasAlpha = !isOpaque(); + nodes[PreviousContentLeaf].opacity = data.opacity() * (1.0 - data.crossFadeProgress()); + nodes[PreviousContentLeaf].coordinateType = NormalizedCoordinates; + } +} + +QMatrix4x4 SceneOpenGL2Window::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 SceneOpenGL2Window::renderSubSurface(GLShader *shader, const QMatrix4x4 &mvp, const QMatrix4x4 &windowMatrix, OpenGLWindowPixmap *pixmap, const QRegion ®ion, bool hardwareClipping) +{ + QMatrix4x4 newWindowMatrix = windowMatrix; + newWindowMatrix.translate(pixmap->subSurface()->position().x(), pixmap->subSurface()->position().y()); + + qreal scale = 1.0; + if (pixmap->surface()) { + scale = pixmap->surface()->scale(); + } + + if (!pixmap->texture()->isNull()) { + setBlendEnabled(pixmap->buffer() && pixmap->buffer()->hasAlphaChannel()); + // render this texture + shader->setUniform(GLShader::ModelViewProjectionMatrix, mvp * newWindowMatrix); + auto texture = pixmap->texture(); + texture->bind(); + texture->render(region, QRect(0, 0, texture->width() / scale, texture->height() / scale), hardwareClipping); + texture->unbind(); + } + + const auto &children = pixmap->children(); + for (auto pixmap : children) { + if (pixmap->subSurface().isNull() || pixmap->subSurface()->surface().isNull() || !pixmap->subSurface()->surface()->isMapped()) { + continue; + } + renderSubSurface(shader, mvp, newWindowMatrix, static_cast(pixmap), region, hardwareClipping); + } +} + +void SceneOpenGL2Window::performPaint(int mask, QRegion region, WindowPaintData data) +{ + if (!beginRenderWindow(mask, region, data)) + return; + + QMatrix4x4 windowMatrix = transformation(mask, data); + const QMatrix4x4 modelViewProjection = modelViewProjectionMatrix(mask, data); + const QMatrix4x4 mvpMatrix = modelViewProjection * windowMatrix; + + GLShader *shader = data.shader; + if (!shader) { + ShaderTraits traits = ShaderTrait::MapTexture; + + 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()); + + const GLenum filter = (mask & (Effect::PAINT_WINDOW_TRANSFORMED | Effect::PAINT_SCREEN_TRANSFORMED)) + && options->glSmoothScale() != 0 ? GL_LINEAR : GL_NEAREST; + + WindowQuadList quads[LeafCount]; + + // Split the quads into separate lists for each type + foreach (const WindowQuad &quad, data.quads) { + switch (quad.type()) { + case WindowQuadDecoration: + quads[DecorationLeaf].append(quad); + continue; + + case WindowQuadContents: + quads[ContentLeaf].append(quad); + continue; + + case WindowQuadShadow: + quads[ShadowLeaf].append(quad); + continue; + + default: + continue; + } + } + + if (data.crossFadeProgress() != 1.0) { + OpenGLWindowPixmap *previous = previousWindowPixmap(); + if (previous) { + const QRect &oldGeometry = previous->contentsRect(); + for (const WindowQuad &quad : quads[ContentLeaf]) { + // we need to create new window quads with normalize 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 = qreal(quad[i].textureX() - toplevel->clientPos().x())/qreal(toplevel->clientSize().width()); + const qreal yFactor = qreal(quad[i].textureY() - toplevel->clientPos().y())/qreal(toplevel->clientSize().height()); + WindowVertex vertex(quad[i].x(), quad[i].y(), + (xFactor * oldGeometry.width() + oldGeometry.x())/qreal(previous->size().width()), + (yFactor * oldGeometry.height() + oldGeometry.y())/qreal(previous->size().height())); + newQuad[i] = vertex; + } + quads[PreviousContentLeaf].append(newQuad); + } + } + } + + const bool indexedQuads = GLVertexBuffer::supportsIndexedQuads(); + const GLenum primitiveType = indexedQuads ? GL_QUADS : GL_TRIANGLES; + const int verticesPerQuad = indexedQuads ? 4 : 6; + + const size_t size = verticesPerQuad * + (quads[0].count() + quads[1].count() + quads[2].count() + quads[3].count()) * sizeof(GLVertex2D); + + GLVertexBuffer *vbo = GLVertexBuffer::streamingBuffer(); + GLVertex2D *map = (GLVertex2D *) vbo->map(size); + + LeafNode nodes[LeafCount]; + setupLeafNodes(nodes, quads, data); + + for (int i = 0, v = 0; i < LeafCount; i++) { + if (quads[i].isEmpty() || !nodes[i].texture) + continue; + + nodes[i].firstVertex = v; + nodes[i].vertexCount = quads[i].count() * verticesPerQuad; + + const QMatrix4x4 matrix = nodes[i].texture->matrix(nodes[i].coordinateType); + + quads[i].makeInterleavedArrays(primitiveType, &map[v], matrix); + v += quads[i].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 < LeafCount; i++) { + if (nodes[i].vertexCount == 0) + continue; + + setBlendEnabled(nodes[i].hasAlpha || nodes[i].opacity < 1.0); + + if (opacity != nodes[i].opacity) { + shader->setUniform(GLShader::ModulationConstant, + modulate(nodes[i].opacity, data.brightness())); + opacity = nodes[i].opacity; + } + + nodes[i].texture->setFilter(filter); + nodes[i].texture->setWrapMode(GL_CLAMP_TO_EDGE); + nodes[i].texture->bind(); + + vbo->draw(region, primitiveType, nodes[i].firstVertex, nodes[i].vertexCount, m_hardwareClipping); + } + + vbo->unbindArrays(); + + // render sub-surfaces + auto wp = windowPixmap(); + const auto &children = wp ? wp->children() : QVector(); + windowMatrix.translate(toplevel->clientPos().x(), toplevel->clientPos().y()); + for (auto pixmap : children) { + if (pixmap->subSurface().isNull() || pixmap->subSurface()->surface().isNull() || !pixmap->subSurface()->surface()->isMapped()) { + continue; + } + renderSubSurface(shader, modelViewProjection, windowMatrix, static_cast(pixmap), region, m_hardwareClipping); + } + + setBlendEnabled(false); + + if (!data.shader) + ShaderManager::instance()->popShader(); + + endRenderWindow(); +} + + +//**************************************** +// 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() +{ +} + +bool OpenGLWindowPixmap::bind() +{ + if (!m_texture->isNull()) { + // always call updateBuffer to get the sub-surface tree updated + if (subSurface().isNull() && !toplevel()->damage().isEmpty()) { + updateBuffer(); + } + auto s = surface(); + if (s && !s->trackedDamage().isEmpty()) { + 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; + } + // also bind all children, needs to be done before checking isValid + // as there might be valid children to render, see https://bugreports.qt.io/browse/QTBUG-52192 + if (subSurface().isNull()) { + updateBuffer(); + } + 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 = NULL; +QPixmap* SceneOpenGL::EffectFrame::m_unstyledPixmap = NULL; + +SceneOpenGL::EffectFrame::EffectFrame(EffectFrameImpl* frame, SceneOpenGL *scene) + : Scene::EffectFrame(frame) + , m_texture(NULL) + , m_textTexture(NULL) + , m_oldTextTexture(NULL) + , m_textPixmap(NULL) + , m_iconTexture(NULL) + , m_oldIconTexture(NULL) + , m_selectionTexture(NULL) + , m_unstyledVBO(NULL) + , 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 = NULL; + delete m_textTexture; + m_textTexture = NULL; + delete m_textPixmap; + m_textPixmap = NULL; + delete m_iconTexture; + m_iconTexture = NULL; + delete m_selectionTexture; + m_selectionTexture = NULL; + delete m_unstyledVBO; + m_unstyledVBO = NULL; + delete m_oldIconTexture; + m_oldIconTexture = NULL; + delete m_oldTextTexture; + m_oldTextTexture = NULL; +} + +void SceneOpenGL::EffectFrame::freeIconFrame() +{ + delete m_iconTexture; + m_iconTexture = NULL; +} + +void SceneOpenGL::EffectFrame::freeTextFrame() +{ + delete m_textTexture; + m_textTexture = NULL; + delete m_textPixmap; + m_textPixmap = NULL; +} + +void SceneOpenGL::EffectFrame::freeSelection() +{ + delete m_selectionTexture; + m_selectionTexture = NULL; +} + +void SceneOpenGL::EffectFrame::crossFadeIcon() +{ + delete m_oldIconTexture; + m_oldIconTexture = m_iconTexture; + m_iconTexture = NULL; +} + +void SceneOpenGL::EffectFrame::crossFadeText() +{ + delete m_oldTextTexture; + m_oldTextTexture = m_textTexture; + m_textTexture = NULL; +} + +void SceneOpenGL::EffectFrame::render(QRegion region, double opacity, double frameOpacity) +{ + if (m_effectFrame->geometry().isEmpty()) + return; // Nothing to display + + 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 = 0L; + 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 = 0L; + delete m_textPixmap; + m_textPixmap = 0L; + + 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 = 0L; + delete m_unstyledPixmap; + m_unstyledPixmap = 0L; + // 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 = NULL; + delete m_unstyledPixmap; + m_unstyledPixmap = NULL; +} + +//**************************************** +// 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(); + 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() +{ + if (effects) { + effects->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 + effects->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, shadowPixmap(ShadowElementTopLeft)); + p.drawPixmap(innerRectLeft, 0, shadowPixmap(ShadowElementTop)); + p.drawPixmap(width - topRight.width(), 0, shadowPixmap(ShadowElementTopRight)); + + p.drawPixmap(0, innerRectTop, shadowPixmap(ShadowElementLeft)); + p.drawPixmap(width - right.width(), innerRectTop, shadowPixmap(ShadowElementRight)); + + p.drawPixmap(0, height - bottomLeft.height(), shadowPixmap(ShadowElementBottomLeft)); + p.drawPixmap(innerRectLeft, height - bottom.height(), shadowPixmap(ShadowElementBottom)); + p.drawPixmap(width - bottomRight.width(), height - 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; + } + } + + effects->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() = default; + +// 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; +} + +void SceneOpenGLDecorationRenderer::render() +{ + const QRegion scheduled = getScheduled(); + const bool dirty = areImageSizesDirty(); + if (scheduled.isEmpty() && !dirty) { + return; + } + if (dirty) { + 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); + + const QRect geometry = dirty ? QRect(QPoint(0, 0), client()->client()->geometry().size()) : scheduled.boundingRect(); + + auto renderPart = [this](const QRect &geo, const QRect &partRect, const QPoint &offset, bool rotated = false) { + if (geo.isNull()) { + return; + } + QImage image = renderToImage(geo); + if (rotated) { + // TODO: get this done directly when rendering to the image + image = rotate(image, QRect(geo.topLeft() - partRect.topLeft(), geo.size())); + } + m_texture->update(image, (geo.topLeft() - partRect.topLeft() + offset) * image.devicePixelRatio()); + }; + renderPart(left.intersected(geometry), left, QPoint(0, top.height() + bottom.height() + 2), true); + renderPart(top.intersected(geometry), top, QPoint(0, 0)); + renderPart(right.intersected(geometry), right, QPoint(0, top.height() + bottom.height() + left.width() + 3), true); + renderPart(bottom.intersected(geometry), bottom, QPoint(0, top.height() + 1)); +} + +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() + 3; + + 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..6453d3b --- /dev/null +++ b/plugins/scenes/opengl/scene_opengl.h @@ -0,0 +1,357 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2009, 2010, 2011 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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; + class Window; + virtual ~SceneOpenGL(); + virtual bool initFailed() const; + virtual bool hasPendingFlush() const; + virtual qint64 paint(QRegion damage, ToplevelList windows); + virtual Scene::EffectFrame *createEffectFrame(EffectFrameImpl *frame); + virtual Shadow *createShadow(Toplevel *toplevel); + virtual void screenGeometryChanged(const QSize &size); + virtual OverlayWindow *overlayWindow(); + virtual bool usesOverlayWindow() const; + virtual bool blocksForRetrace() const; + virtual bool syncsToVBlank() const; + virtual bool makeOpenGLContextCurrent() override; + virtual void doneOpenGLContextCurrent() override; + Decoration::Renderer *createDecorationRenderer(Decoration::DecoratedClientImpl *impl) override; + virtual void triggerFence() override; + virtual QMatrix4x4 projectionMatrix() const = 0; + bool animationsSupported() const override; + + void insertWait(); + + void idle(); + + 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; + + static SceneOpenGL *createScene(QObject *parent); + +protected: + SceneOpenGL(OpenGLBackend *backend, QObject *parent = nullptr); + virtual void paintBackground(QRegion region); + virtual void extendPaintRegion(QRegion ®ion, bool opaqueFullscreen); + QMatrix4x4 transformation(int mask, const ScreenPaintData &data) const; + virtual void paintDesktop(int desktop, int mask, const QRegion ®ion, ScreenPaintData &data); + + 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); + virtual ~SceneOpenGL2(); + virtual CompositingType compositingType() const { + return OpenGL2Compositing; + } + + static bool supported(OpenGLBackend *backend); + + QMatrix4x4 projectionMatrix() const override { return m_projectionMatrix; } + QMatrix4x4 screenProjectionMatrix() const override { return m_screenProjectionMatrix; } + +protected: + virtual void paintSimpleScreen(int mask, QRegion region); + virtual void paintGenericScreen(int mask, ScreenPaintData data); + virtual void doPaintBackground(const QVector< float >& vertices); + virtual Scene::Window *createWindow(Toplevel *t); + virtual void finalDrawWindow(EffectWindowImpl* w, int mask, QRegion region, WindowPaintData& data); + virtual void updateProjectionMatrix() override; + void paintCursor() override; + +private: + void performPaintWindow(EffectWindowImpl* w, int mask, QRegion region, WindowPaintData& data); + QMatrix4x4 createProjectionMatrix() const; + +private: + LanczosFilter *m_lanczosFilter; + QScopedPointer m_cursorTexture; + QMatrix4x4 m_projectionMatrix; + QMatrix4x4 m_screenProjectionMatrix; + GLuint vao; +}; + +class SceneOpenGL::Window + : public Scene::Window +{ +public: + virtual ~Window(); + bool beginRenderWindow(int mask, const QRegion ®ion, WindowPaintData &data); + virtual void performPaint(int mask, QRegion region, WindowPaintData data) = 0; + void endRenderWindow(); + bool bindTexture(); + void setScene(SceneOpenGL *scene) { + m_scene = scene; + } + +protected: + virtual WindowPixmap* createWindowPixmap(); + Window(Toplevel* c); + enum TextureType { + Content, + Decoration, + Shadow + }; + + QMatrix4x4 transformation(int mask, const WindowPaintData &data) const; + GLTexture *getDecorationTexture() const; + +protected: + SceneOpenGL *m_scene; + bool m_hardwareClipping; +}; + +class OpenGLWindowPixmap; + +class SceneOpenGL2Window : public SceneOpenGL::Window +{ +public: + enum Leaf { ShadowLeaf = 0, DecorationLeaf, ContentLeaf, PreviousContentLeaf, LeafCount }; + + struct LeafNode + { + LeafNode() + : texture(0), + firstVertex(0), + vertexCount(0), + opacity(1.0), + hasAlpha(false), + coordinateType(UnnormalizedCoordinates) + { + } + + GLTexture *texture; + int firstVertex; + int vertexCount; + float opacity; + bool hasAlpha; + TextureCoordinateType coordinateType; + }; + + explicit SceneOpenGL2Window(Toplevel *c); + virtual ~SceneOpenGL2Window(); + +protected: + QMatrix4x4 modelViewProjectionMatrix(int mask, const WindowPaintData &data) const; + QVector4D modulate(float opacity, float brightness) const; + void setBlendEnabled(bool enabled); + void setupLeafNodes(LeafNode *nodes, const WindowQuadList *quads, const WindowPaintData &data); + virtual void performPaint(int mask, QRegion region, WindowPaintData data); + +private: + void renderSubSurface(GLShader *shader, const QMatrix4x4 &mvp, const QMatrix4x4 &windowMatrix, OpenGLWindowPixmap *pixmap, const QRegion ®ion, bool hardwareClipping); + /** + * Whether prepareStates enabled blending and restore states should disable again. + **/ + bool m_blendingEnabled; +}; + +class OpenGLWindowPixmap : public WindowPixmap +{ +public: + explicit OpenGLWindowPixmap(Scene::Window *window, SceneOpenGL *scene); + virtual ~OpenGLWindowPixmap(); + 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); + virtual ~EffectFrame(); + + virtual void free(); + virtual void freeIconFrame(); + virtual void freeTextFrame(); + virtual void freeSelection(); + + virtual void render(QRegion region, double opacity, double frameOpacity); + + virtual void crossFadeIcon(); + virtual void crossFadeText(); + + 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); + virtual ~SceneOpenGLShadow(); + + GLTexture *shadowTexture() { + return m_texture.data(); + } +protected: + virtual void buildQuads(); + virtual bool prepareBackend(); +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); + virtual ~SceneOpenGLDecorationRenderer(); + + 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/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..0d38a43 --- /dev/null +++ b/plugins/scenes/qpainter/qpainter.json @@ -0,0 +1,73 @@ +{ + "CompositingType": 4, + "KPlugin": { + "Description": "KWin Compositor plugin rendering through QPainter", + "Description[ca@valencia]": "Connector del Compositor del KWin que renderitza a través del 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[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[id]": "Plugin KWin Compositor perenderan melalui QPainter", + "Description[it]": "Estensione del compositore di KWin per la resa tramite QPainter", + "Description[ko]": "QPainter로 렌더링하는 KWin 컴포지터 플러그인", + "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[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[eu]": "SceneQPainter", + "Name[fi]": "SceneQPainter", + "Name[fr]": "SceneQPainter", + "Name[gl]": "SceneQPainter", + "Name[id]": "SceneQPainter", + "Name[it]": "SceneQPainter", + "Name[ko]": "SceneQPainter", + "Name[nl]": "SceneQPainter", + "Name[nn]": "SceneQPainter", + "Name[pl]": "QPainter sceny", + "Name[pt]": "SceneQPainter", + "Name[pt_BR]": "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..7335d15 --- /dev/null +++ b/plugins/scenes/qpainter/scene_qpainter.cpp @@ -0,0 +1,876 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "scene_qpainter.h" +// KWin +#include "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 "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 NULL; + } + 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, 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(QRegion damage, ToplevelList toplevels) +{ + QElapsedTimer renderTimer; + renderTimer.start(); + + createStackingOrder(toplevels); + + 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(); + + 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(); + m_backend->showOverlay(); + + m_painter->end(); + m_backend->present(mask, updateRegion); + } + + // do cleanup + clearStackingOrder(); + + emit frameRendered(); + + return renderTimer.nsecsElapsed(); +} + +void SceneQPainter::paintBackground(QRegion region) +{ + m_painter->setBrush(Qt::black); + m_painter->drawRects(region.rects()); +} + +void SceneQPainter::paintCursor() +{ + if (!kwinApp()->platform()->usesSoftwareCursor()) { + return; + } + const QImage img = kwinApp()->platform()->softwareCursor(); + if (img.isNull()) { + return; + } + const QPoint cursorPos = Cursor::pos(); + const QPoint hotspot = kwinApp()->platform()->softwareCursorHotspot(); + m_painter->drawImage(cursorPos - hotspot, img); + kwinApp()->platform()->markCursorAsRendered(); +} + +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() +{ + discardShape(); +} + +static void paintSubSurface(QPainter *painter, const QPoint &pos, QPainterWindowPixmap *pixmap) +{ + QPoint p = pos; + if (!pixmap->subSurface().isNull()) { + p += pixmap->subSurface()->position(); + } + + painter->drawImage(QRect(pos, pixmap->size()), pixmap->image()); + const auto &children = pixmap->children(); + for (auto it = children.begin(); it != children.end(); ++it) { + auto pixmap = static_cast(*it); + if (pixmap->subSurface().isNull() || pixmap->subSurface()->surface().isNull() || !pixmap->subSurface()->surface()->isMapped()) { + continue; + } + paintSubSurface(painter, p, pixmap); + } +} + +void SceneQPainter::Window::performPaint(int mask, QRegion region, WindowPaintData data) +{ + if (!(mask & (PAINT_WINDOW_TRANSFORMED | PAINT_SCREEN_TRANSFORMED))) + region &= toplevel->visibleRect(); + + if (region.isEmpty()) + return; + QPainterWindowPixmap *pixmap = windowPixmap(); + if (!pixmap || !pixmap->isValid()) { + return; + } + if (!toplevel->damage().isEmpty()) { + pixmap->updateBuffer(); + 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->geometry().topLeft() - toplevel->visibleRect().topLeft()); + painter = &tempPainter; + } + renderShadow(painter); + renderWindowDecorations(painter); + + // render content + const QRect target = QRect(toplevel->clientPos(), toplevel->clientSize()); + QSize srcSize = pixmap->image().size(); + if (pixmap->surface() && pixmap->surface()->scale() == 1 && srcSize != toplevel->clientSize()) { + // special case for XWayland windows + srcSize = toplevel->clientSize(); + } + const QRect src = QRect(toplevel->clientPos() + toplevel->clientContentPos(), srcSize); + painter->drawImage(target, pixmap->image(), src); + + // render subsurfaces + const auto &children = pixmap->children(); + for (auto pixmap : children) { + if (pixmap->subSurface().isNull() || pixmap->subSurface()->surface().isNull() || !pixmap->subSurface()->surface()->isMapped()) { + continue; + } + paintSubSurface(painter, toplevel->clientPos(), static_cast(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->geometry().topLeft(), tempImage); + } + + painter->restore(); +} + +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; + } + // 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::updateBuffer() +{ + const auto oldBuffer = buffer(); + WindowPixmap::updateBuffer(); + const auto &b = buffer(); + 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(QRegion region, 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); + painter.drawPixmap(topLeft.width(), 0, top); + painter.drawPixmap(width - topRight.width(), 0, topRight); + painter.drawPixmap(0, height - bottomLeft.height(), bottomLeft); + painter.drawPixmap(bottomLeft.width(), height - bottom.height(), bottom); + painter.drawPixmap(width - bottomRight.width(), height - bottomRight.height(), bottomRight); + painter.drawPixmap(0, topLeft.height(), left); + painter.drawPixmap(width - right.width(), topRight.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..e7c9f9a --- /dev/null +++ b/plugins/scenes/qpainter/scene_qpainter.h @@ -0,0 +1,202 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#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: + virtual ~SceneQPainter(); + virtual bool usesOverlayWindow() const override; + virtual OverlayWindow* overlayWindow() override; + virtual qint64 paint(QRegion damage, ToplevelList windows) override; + virtual void paintGenericScreen(int mask, ScreenPaintData data) override; + virtual CompositingType compositingType() const override; + virtual bool initFailed() const override; + virtual EffectFrame *createEffectFrame(EffectFrameImpl *frame) override; + virtual 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: + virtual void paintBackground(QRegion region) override; + virtual Scene::Window *createWindow(Toplevel *toplevel) override; + void paintCursor() override; + +private: + explicit SceneQPainter(QPainterBackend *backend, QObject *parent = nullptr); + QScopedPointer m_backend; + QScopedPointer m_painter; + class Window; +}; + +class SceneQPainter::Window : public Scene::Window +{ +public: + Window(SceneQPainter *scene, Toplevel *c); + virtual ~Window(); + virtual void performPaint(int mask, QRegion region, WindowPaintData data) override; +protected: + virtual WindowPixmap *createWindowPixmap() override; +private: + void renderShadow(QPainter *painter); + void renderWindowDecorations(QPainter *painter); + SceneQPainter *m_scene; +}; + +class QPainterWindowPixmap : public WindowPixmap +{ +public: + explicit QPainterWindowPixmap(Scene::Window *window); + virtual ~QPainterWindowPixmap(); + virtual void create() override; + bool isValid() const override; + + void updateBuffer() override; + const QImage &image(); + +protected: + WindowPixmap *createChild(const QPointer &subSurface) override; +private: + explicit QPainterWindowPixmap(const QPointer &subSurface, WindowPixmap *parent); + QImage m_image; +}; + +class QPainterEffectFrame : public Scene::EffectFrame +{ +public: + QPainterEffectFrame(EffectFrameImpl *frame, SceneQPainter *scene); + virtual ~QPainterEffectFrame(); + virtual void crossFadeIcon() override {} + virtual void crossFadeText() override {} + virtual void free() override {} + virtual void freeIconFrame() override {} + virtual void freeTextFrame() override {} + virtual void freeSelection() override {} + virtual void render(QRegion region, double opacity, double frameOpacity) override; +private: + SceneQPainter *m_scene; +}; + +class SceneQPainterShadow : public Shadow +{ +public: + SceneQPainterShadow(Toplevel* toplevel); + virtual ~SceneQPainterShadow(); + + QImage &shadowTexture() { + return m_texture; + } + +protected: + virtual void buildQuads() override; + virtual 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); + virtual ~SceneQPainterDecorationRenderer(); + + 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() +{ + 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..30911f1 --- /dev/null +++ b/plugins/scenes/xrender/scene_xrender.cpp @@ -0,0 +1,1326 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak +Copyright (C) 2009 Fredrik Höglund +Copyright (C) 2013 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "scene_xrender.h" + +#include "utils.h" + +#ifdef KWIN_HAVE_XRENDER_COMPOSITING + +#include "logging.h" +#include "toplevel.h" +#include "client.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 "kwinxrenderutils.h" +#include "decorations/decoratedclient.h" + +#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 NULL; +} + +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()), NULL)); + 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, NULL); + } else { + // create XRender picture for the root window + m_format = XRenderUtils::findPictFormat(defaultScreen()->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, NULL); + 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 NULL; + } + 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(QRegion damage, ToplevelList 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, 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(QRegion region) +{ + 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 = 0; +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() +{ + discardShape(); +} + +void SceneXrender::Window::cleanup() +{ + delete s_tempPicture; + s_tempPicture = NULL; + 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; +} + +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 = NULL; + 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, QRegion region, WindowPaintData data) +{ + 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()); // Client rect (in the window) + qreal xscale = 1; + qreal yscale = 1; + bool scaled = false; + + Client *client = dynamic_cast(toplevel); + Deleted *deleted = dynamic_cast(toplevel); + const QRect decorationRect = toplevel->decorationRect(); + if (((client && !client->noBorder()) || (deleted && !deleted->noBorder())) && + true) { + // decorated client + transformed_shape = decorationRect; + if (toplevel->shape()) { + // "xeyes" + decoration + transformed_shape -= cr; + transformed_shape += shape(); + } + } else { + transformed_shape = shape(); + } + if (toplevel->hasShadow()) + 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 = transformed_shape.rects(); + for (int i = 0; i < rects.count(); ++i) { + QRect& r = rects[ i ]; + r.setRect(qRound(r.x() * xscale), qRound(r.y() * yscale), + qRound(r.width() * xscale), qRound(r.height() * yscale)); + } + 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, 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, NULL); +} + +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, NULL); +} + +//**************************************** +// SceneXrender::EffectFrame +//**************************************** + +XRenderPicture *SceneXrender::EffectFrame::s_effectFrameCircle = NULL; + +SceneXrender::EffectFrame::EffectFrame(EffectFrameImpl* frame) + : Scene::EffectFrame(frame) +{ + m_picture = NULL; + m_textPicture = NULL; + m_iconPicture = NULL; + m_selectionPicture = NULL; +} + +SceneXrender::EffectFrame::~EffectFrame() +{ + delete m_picture; + delete m_textPicture; + delete m_iconPicture; + delete m_selectionPicture; +} + +void SceneXrender::EffectFrame::cleanup() +{ + delete s_effectFrameCircle; + s_effectFrameCircle = NULL; +} + +void SceneXrender::EffectFrame::free() +{ + delete m_picture; + m_picture = NULL; + delete m_textPicture; + m_textPicture = NULL; + delete m_iconPicture; + m_iconPicture = NULL; + delete m_selectionPicture; + m_selectionPicture = NULL; +} + +void SceneXrender::EffectFrame::freeIconFrame() +{ + delete m_iconPicture; + m_iconPicture = NULL; +} + +void SceneXrender::EffectFrame::freeTextFrame() +{ + delete m_textPicture; + m_textPicture = NULL; +} + +void SceneXrender::EffectFrame::freeSelection() +{ + delete m_selectionPicture; + m_selectionPicture = NULL; +} + +void SceneXrender::EffectFrame::crossFadeIcon() +{ + // TODO: implement me +} + +void SceneXrender::EffectFrame::crossFadeText() +{ + // TODO: implement me +} + +void SceneXrender::EffectFrame::render(QRegion region, 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 = 0L; + 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 = 0L; + + 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(); + scheduled = client()->client()->decorationRect(); + } + + 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.isNull()) { + 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.byteCount(), 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() +{ + +} diff --git a/plugins/scenes/xrender/scene_xrender.h b/plugins/scenes/xrender/scene_xrender.h new file mode 100644 index 0000000..04bc66c --- /dev/null +++ b/plugins/scenes/xrender/scene_xrender.h @@ -0,0 +1,361 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2006 Lubos Lunak + +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, see . +*********************************************************************/ + +#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(); + + virtual void present(int mask, const QRegion &damage); + virtual OverlayWindow* overlayWindow(); + virtual void showOverlay(); + virtual void screenGeometryChanged(const QSize &size); + virtual bool usesOverlayWindow() const; +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; + virtual ~SceneXrender(); + virtual bool initFailed() const; + virtual CompositingType compositingType() const { + return XRenderCompositing; + } + virtual qint64 paint(QRegion damage, ToplevelList windows); + virtual Scene::EffectFrame *createEffectFrame(EffectFrameImpl *frame); + virtual Shadow *createShadow(Toplevel *toplevel); + virtual void screenGeometryChanged(const QSize &size); + xcb_render_picture_t xrenderBufferPicture() const override; + virtual OverlayWindow *overlayWindow() { + return m_backend->overlayWindow(); + } + virtual bool usesOverlayWindow() const { + return m_backend->usesOverlayWindow(); + } + Decoration::Renderer *createDecorationRenderer(Decoration::DecoratedClientImpl *client); + + bool animationsSupported() const override { + return true; + } + + static SceneXrender *createScene(QObject *parent); +protected: + virtual Scene::Window *createWindow(Toplevel *toplevel); + virtual void paintBackground(QRegion region); + virtual void paintGenericScreen(int mask, ScreenPaintData data); + virtual void paintDesktop(int desktop, int mask, const QRegion ®ion, ScreenPaintData &data); + void paintCursor() override; +private: + explicit SceneXrender(XRenderBackend *backend, QObject *parent = nullptr); + static ScreenPaintData screen_paint; + class Window; + QScopedPointer m_backend; +}; + +class SceneXrender::Window + : public Scene::Window +{ +public: + Window(Toplevel* c, SceneXrender *scene); + virtual ~Window(); + virtual void performPaint(int mask, QRegion region, WindowPaintData data); + QRegion transformedShape() const; + void setTransformedShape(const QRegion& shape); + static void cleanup(); +protected: + virtual WindowPixmap* createWindowPixmap(); +private: + QRect mapToScreen(int mask, const WindowPaintData &data, const QRect &rect) const; + QPoint mapToScreen(int mask, const WindowPaintData &data, const QPoint &point) 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); + virtual ~XRenderWindowPixmap(); + xcb_render_picture_t picture() const; + virtual void create(); +private: + xcb_render_picture_t m_picture; + xcb_render_pictformat_t m_format; +}; + +class SceneXrender::EffectFrame + : public Scene::EffectFrame +{ +public: + EffectFrame(EffectFrameImpl* frame); + virtual ~EffectFrame(); + + virtual void free(); + virtual void freeIconFrame(); + virtual void freeTextFrame(); + virtual void freeSelection(); + virtual void crossFadeIcon(); + virtual void crossFadeText(); + virtual void render(QRegion region, double opacity, double frameOpacity); + 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; + virtual ~SceneXRenderShadow(); + + 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: + virtual void buildQuads(); + virtual bool prepareBackend(); +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); + virtual ~SceneXRenderDecorationRenderer(); + + 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..d7f339d --- /dev/null +++ b/plugins/scenes/xrender/xrender.json @@ -0,0 +1,73 @@ +{ + "CompositingType": 2, + "KPlugin": { + "Description": "KWin Compositor plugin rendering through XRender", + "Description[ca@valencia]": "Connector del Compositor del KWin que renderitza a través del 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[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[id]": "Plugin KWin Compositor perenderan melalui XRender", + "Description[it]": "Estensione del compositore di KWin per la resa tramite XRender", + "Description[ko]": "XRender로 렌더링하는 KWin 컴포지터 플러그인", + "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[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[eu]": "SceneXRender", + "Name[fi]": "SceneXRender", + "Name[fr]": "SceneXRender", + "Name[gl]": "SceneXRender", + "Name[id]": "SceneXRender", + "Name[it]": "SceneXRender", + "Name[ko]": "SceneXRender", + "Name[nl]": "SceneXRender", + "Name[nn]": "SceneXRender", + "Name[pl]": "XRender sceny", + "Name[pt]": "SceneXRender", + "Name[pt_BR]": "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/po/af/kcmkwindecoration.po b/po/af/kcmkwindecoration.po new file mode 100644 index 0000000..c30ec75 --- /dev/null +++ b/po/af/kcmkwindecoration.po @@ -0,0 +1,206 @@ +# 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: http://bugs.kde.org\n" +"POT-Creation-Date: 2018-09-14 06:48+0200\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" + +#: declarative-plugin/buttonsmodel.cpp:68 +#, kde-format +msgid "Menu" +msgstr "Kieslys" + +#: declarative-plugin/buttonsmodel.cpp:70 +#, kde-format +msgid "Application menu" +msgstr "" + +#: declarative-plugin/buttonsmodel.cpp:72 +#, fuzzy, kde-format +#| msgid "On All Desktops" +msgid "On all desktops" +msgstr "Op alle werkskerms" + +#: declarative-plugin/buttonsmodel.cpp:74 +#, kde-format +msgid "Minimize" +msgstr "Minimeer" + +#: declarative-plugin/buttonsmodel.cpp:76 +#, kde-format +msgid "Maximize" +msgstr "Maksimeer" + +#: declarative-plugin/buttonsmodel.cpp:78 +#, kde-format +msgid "Close" +msgstr "" + +#: declarative-plugin/buttonsmodel.cpp:80 +#, kde-format +msgid "Context help" +msgstr "" + +#: declarative-plugin/buttonsmodel.cpp:82 +#, kde-format +msgid "Shade" +msgstr "Verskadu" + +#: declarative-plugin/buttonsmodel.cpp:84 +#, fuzzy, kde-format +#| msgid "Keep Below Others" +msgid "Keep below" +msgstr "Hou Onder ander" + +#: declarative-plugin/buttonsmodel.cpp:86 +#, fuzzy, kde-format +#| msgid "Keep Above Others" +msgid "Keep above" +msgstr "Hou Bo ander" + +#: kcm.cpp:141 +#, kde-format +msgid "" +"Close by double clicking:\n" +" To open the menu, keep the button pressed until it appears." +msgstr "" + +#. i18n: ectx: attribute (title), widget (QWidget, theTab) +#: kcm.ui:21 +#, kde-format +msgid "Theme" +msgstr "" + +#. i18n: ectx: property (placeholderText), widget (QLineEdit, filter) +#: kcm.ui:29 +#, kde-format +msgid "Search" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, knsButton) +#: kcm.ui:42 +#, fuzzy, kde-format +#| msgid "&Window Decoration" +msgid "Get New Decorations..." +msgstr "Venster Versiering" + +#. i18n: ectx: property (text), widget (QCheckBox, closeWindowsDoubleClick) +#: kcm.ui:54 +#, kde-format +msgid "Close windows by double clicking &the menu button" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, borderSizesLabel) +#: kcm.ui:95 +#, fuzzy, kde-format +#| msgid "B&order size:" +msgid "Border si&ze:" +msgstr "Rant grootte:" + +#. i18n: ectx: property (text), item, widget (KComboBox, borderSizesCombo) +#: kcm.ui:109 +#, fuzzy, kde-format +#| msgid "B&order size:" +msgctxt "@item:inlistbox Border size:" +msgid "No Borders" +msgstr "Rant grootte:" + +#. i18n: ectx: property (text), item, widget (KComboBox, borderSizesCombo) +#: kcm.ui:114 +#, kde-format +msgctxt "@item:inlistbox Border size:" +msgid "No Side Borders" +msgstr "" + +#. i18n: ectx: property (text), item, widget (KComboBox, borderSizesCombo) +#: kcm.ui:119 +#, fuzzy, kde-format +#| msgid "Tiny" +msgctxt "@item:inlistbox Border size:" +msgid "Tiny" +msgstr "Klein" + +#. i18n: ectx: property (text), item, widget (KComboBox, borderSizesCombo) +#: kcm.ui:124 +#, fuzzy, kde-format +#| msgid "Normal" +msgctxt "@item:inlistbox Border size:" +msgid "Normal" +msgstr "Normaal" + +#. i18n: ectx: property (text), item, widget (KComboBox, borderSizesCombo) +#: kcm.ui:129 +#, fuzzy, kde-format +#| msgid "Large" +msgctxt "@item:inlistbox Border size:" +msgid "Large" +msgstr "Groot" + +#. i18n: ectx: property (text), item, widget (KComboBox, borderSizesCombo) +#: kcm.ui:134 +#, fuzzy, kde-format +#| msgid "Very Large" +msgctxt "@item:inlistbox Border size:" +msgid "Very Large" +msgstr "Baie Groot" + +#. i18n: ectx: property (text), item, widget (KComboBox, borderSizesCombo) +#: kcm.ui:139 +#, fuzzy, kde-format +#| msgid "Huge" +msgctxt "@item:inlistbox Border size:" +msgid "Huge" +msgstr "Groot" + +#. i18n: ectx: property (text), item, widget (KComboBox, borderSizesCombo) +#: kcm.ui:144 +#, fuzzy, kde-format +#| msgid "Very Huge" +msgctxt "@item:inlistbox Border size:" +msgid "Very Huge" +msgstr "Baie groot" + +#. i18n: ectx: property (text), item, widget (KComboBox, borderSizesCombo) +#: kcm.ui:149 +#, fuzzy, kde-format +#| msgid "Oversized" +msgctxt "@item:inlistbox Border size:" +msgid "Oversized" +msgstr "Oorgrote" + +#. i18n: ectx: attribute (title), widget (QWidget, tab_2) +#: kcm.ui:160 +#, kde-format +msgid "Buttons" +msgstr "Knoppies" + +#: qml/Buttons.qml:73 +#, kde-format +msgid "Titlebar" +msgstr "" + +#: qml/Buttons.qml:200 +#, kde-format +msgid "Drop here to remove button" +msgstr "" + +#: qml/Buttons.qml:224 +#, kde-format +msgid "Drag buttons between here and the titlebar" +msgstr "" + +#: qml/Previews.qml:138 +#, fuzzy, kde-format +#| msgid "&Window Decoration" +msgid "Configure %1..." +msgstr "Venster Versiering" \ No newline at end of file diff --git a/po/af/kcmkwinrules.po b/po/af/kcmkwinrules.po new file mode 100644 index 0000000..b4065ff --- /dev/null +++ b/po/af/kcmkwinrules.po @@ -0,0 +1,1349 @@ +# UTF-8 test:äëïöü +msgid "" +msgstr "" +"Project-Id-Version: kcmkwinrules stable\n" +"Report-Msgid-Bugs-To: http://bugs.kde.org\n" +"POT-Creation-Date: 2018-10-13 06:43+0200\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" + +#. i18n: ectx: property (text), item, widget (QListWidget, types) +#. i18n: ectx: property (text), item, widget (QComboBox, type) +#: detectwidget.cpp:75 ruleswidgetbase.ui:428 ruleswidgetbase.ui:2459 +#, kde-format +msgid "Normal Window" +msgstr "Normale Venster" + +#. i18n: ectx: property (text), item, widget (QListWidget, types) +#. i18n: ectx: property (text), item, widget (QComboBox, type) +#: detectwidget.cpp:76 ruleswidgetbase.ui:463 ruleswidgetbase.ui:2494 +#, kde-format +msgid "Desktop" +msgstr "Werkskerm" + +#. i18n: ectx: property (text), item, widget (QListWidget, types) +#. i18n: ectx: property (text), item, widget (QComboBox, type) +#: detectwidget.cpp:77 ruleswidgetbase.ui:443 ruleswidgetbase.ui:2474 +#, kde-format +msgid "Dock (panel)" +msgstr "Meer vas (paneel)" + +#. i18n: ectx: property (text), item, widget (QListWidget, types) +#. i18n: ectx: property (text), item, widget (QComboBox, type) +#: detectwidget.cpp:78 ruleswidgetbase.ui:448 ruleswidgetbase.ui:2479 +#, kde-format +msgid "Toolbar" +msgstr "Nutsbalk" + +#. i18n: ectx: property (text), item, widget (QListWidget, types) +#. i18n: ectx: property (text), item, widget (QComboBox, type) +#: detectwidget.cpp:79 ruleswidgetbase.ui:453 ruleswidgetbase.ui:2484 +#, kde-format +msgid "Torn-Off Menu" +msgstr "Afskeur kieslys" + +#. i18n: ectx: property (text), item, widget (QListWidget, types) +#. i18n: ectx: property (text), item, widget (QComboBox, type) +#: detectwidget.cpp:80 ruleswidgetbase.ui:433 ruleswidgetbase.ui:2464 +#, kde-format +msgid "Dialog Window" +msgstr "Dialoog Venster" + +#: detectwidget.cpp:81 +#, kde-format +msgid "Override Type" +msgstr "Omverwerp Tipe" + +#. i18n: ectx: property (text), item, widget (QListWidget, types) +#. i18n: ectx: property (text), item, widget (QComboBox, type) +#: detectwidget.cpp:82 ruleswidgetbase.ui:473 ruleswidgetbase.ui:2499 +#, kde-format +msgid "Standalone Menubar" +msgstr "Alleenstaande Kiesbalk" + +#. i18n: ectx: property (text), item, widget (QListWidget, types) +#. i18n: ectx: property (text), item, widget (QComboBox, type) +#: detectwidget.cpp:83 ruleswidgetbase.ui:438 ruleswidgetbase.ui:2469 +#, kde-format +msgid "Utility Window" +msgstr "Nuts Venster" + +#. i18n: ectx: property (text), item, widget (QListWidget, types) +#. i18n: ectx: property (text), item, widget (QComboBox, type) +#: detectwidget.cpp:84 ruleswidgetbase.ui:458 ruleswidgetbase.ui:2489 +#, kde-format +msgid "Splash Screen" +msgstr "Spat Skerm" + +#: detectwidget.cpp:90 +#, kde-format +msgid "Unknown - will be treated as Normal Window" +msgstr "Onbekend - sal as 'n normale venster behandel word" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox_2) +#: detectwidget.ui:17 +#, kde-format +msgid "Information About Selected Window" +msgstr "Informasie aangaande Gekose Venster" + +#. i18n: ectx: property (text), widget (QLabel, textLabel1) +#: detectwidget.ui:29 +#, kde-format +msgid "Class:" +msgstr "Klas" + +#. i18n: ectx: property (text), widget (QLabel, textLabel3) +#: detectwidget.ui:57 +#, kde-format +msgid "Role:" +msgstr "Rol" + +#. i18n: ectx: property (text), widget (QLabel, textLabel4) +#: detectwidget.ui:85 +#, kde-format +msgid "Type:" +msgstr "Tipe" + +#. i18n: ectx: property (text), widget (QLabel, textLabel8) +#: detectwidget.ui:113 +#, kde-format +msgid "Title:" +msgstr "Titel" + +#. i18n: ectx: property (text), widget (QLabel, textLabel13) +#: detectwidget.ui:141 +#, kde-format +msgid "Machine:" +msgstr "Masjien" + +#. i18n: ectx: property (title), widget (QGroupBox, groupBox) +#: detectwidget.ui:172 +#, kde-format +msgid "Match by primary class name and" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, match_whole_class) +#: detectwidget.ui:181 +#, kde-format +msgid "Secondary class name (resulting in term in brackets)" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, match_role) +#: detectwidget.ui:188 +#, kde-format +msgid "Window role (can be used to select windows by function)" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, match_type) +#: detectwidget.ui:195 +#, kde-format +msgid "Window type (eg. all dialogs, but not the main windows)" +msgstr "" + +#. i18n: ectx: property (text), widget (QCheckBox, match_title) +#: detectwidget.ui:202 +#, kde-format +msgid "" +"Window title (very specific, can fail due to content changes or translation)" +msgstr "" + +#. i18n: ectx: property (text), widget (QLabel, textLabel2) +#: editshortcut.ui:18 +#, fuzzy, kde-format +#| msgid "" +#| "A single shortcut can be easily assigned or cleared using the two " +#| "buttons. Only shortcuts with modifiers can be used.

\n" +#| "It is possible to have several possible shortcuts, and the first " +#| "available shortcut will be used. The shortcuts are specified using space-" +#| "separated shortcut sets. One set is specified as base+(list), where base are modifiers and list is a list of keys.
\n" +#| "For example \"Shift+Alt+(123) Shift+Ctrl+(ABC)\" will first try " +#| "Shift+Alt+1, then others with Shift+Ctrl+C as the last one." +msgid "" +"A single shortcut can be easily assigned or cleared using the two buttons. " +"Only shortcuts with modifiers can be used.

\n" +"It is possible to have several possible shortcuts, and the first available " +"shortcut will be used. The shortcuts are specified using shortcut sets " +"separated by \" - \". One set is specified as base+(list), " +"where base are modifiers and list is a list of keys.
\n" +"For example \"Shift+Alt+(123) Shift+Ctrl+(ABC)\" will first try " +"Shift+Alt+1, then others with Shift+Ctrl+C as the last one." +msgstr "" +"'n Enkele kortpad kan maklik aangewys of skoongemaak word deur die twee " +"knoppies te gebruik. Net kortpaaie met aanpassers kan gebruik word.

\n" +"Dit is moontlik om verskeie kortpaate te hê, en die eerste beskikbare " +"kortpad sal gebruik word. Die kortpaaie word gespesifiseer deur spasie-" +"geskeide kortpad pare te gebruik. Een paar word gespesifiseer as base" +"+(list), waar base aanpassers is en list 'n lys van sleutels.\n" +"byvoorbeeld \"Shift+ALt+(123) Shift+Cntrl+(ABC)\" sal eerste " +"probeerShift+Alt+1, dan ander met Shift+Cntrl+C as die laaste een." + +#. i18n: ectx: property (text), widget (QPushButton, pushButton1) +#: editshortcut.ui:62 +#, kde-format +msgid "&Single Shortcut" +msgstr "Enkel kortpad" + +#. i18n: ectx: property (text), widget (QPushButton, pushButton2) +#: editshortcut.ui:85 +#, kde-format +msgid "C&lear" +msgstr "Skoon" + +#: kcm.cpp:51 +#, kde-format +msgid "Window-Specific Settings Configuration Module" +msgstr "Venster-spesifieke Instelling Konfigurasie-Module " + +#: kcm.cpp:52 +#, kde-format +msgid "(c) 2004 KWin and KControl Authors" +msgstr "(c) 2004 KWIn en KKontrole Outers" + +#: kcm.cpp:53 +#, kde-format +msgid "Lubos Lunak" +msgstr "" + +#: kcm.cpp:84 +#, 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

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 WrDYl}cplFH0000FTOe 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 + +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, see . +*********************************************************************/ +#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->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); + + 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 ToplevelList &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()); + auto newKeyboard = seat->focusedKeyboard(); + if (newKeyboard && newKeyboard->client() == waylandServer()->xWaylandConnection()) { + // focus passed to an XWayland surface + const auto selection = seat->selection(); + auto xclipboard = waylandServer()->xclipboardSyncDataDevice(); + if (xclipboard && selection != xclipboard.data()) { + if (selection) { + xclipboard->sendSelection(selection); + } else { + xclipboard->sendClearSelection(); + } + } + } + } + } 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..83a60c0 --- /dev/null +++ b/keyboard_input.h @@ -0,0 +1,104 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2013, 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~KeyboardInputRedirection(); + + 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..d3697a4 --- /dev/null +++ b/keyboard_layout.cpp @@ -0,0 +1,342 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016, 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#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) { + delete m_dbusInterface; + 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::Active); + m_notifierItem->setToolTipTitle(i18nc("tooltip title", "Keyboard Layout")); + m_notifierItem->setTitle(i18nc("tooltip title", "Keyboard Layout")); + m_notifierItem->setToolTipIconByName(QStringLiteral("preferences-desktop-keyboard")); + m_notifierItem->setStandardActionsEnabled(false); + + // TODO: proper icon + m_notifierItem->setIconByName(QStringLiteral("preferences-desktop-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(); + } + } + ); + + m_notifierItem->setStatus(KStatusNotifierItem::Active); +} + +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 QString policyKey = m_config->group(QStringLiteral("Layout")).readEntry("SwitchMode", QStringLiteral("Global")); + if (!m_policy || m_policy->name() != policyKey) { + delete m_policy; + m_policy = KeyboardLayoutSwitching::Policy::create(m_xkb, this, policyKey); + } + } + m_xkb->reconfigure(); + resetLayout(); +} + +void KeyboardLayout::resetLayout() +{ + m_layout = m_xkb->currentLayout(); + initNotifierItem(); + updateNotifier(); + reinitNotifierMenu(); + loadShortcuts(); + emit layoutsReconfigured(); + + initDBusInterface(); +} + +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, static_cast(&QProcess::error), 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..643bdbf --- /dev/null +++ b/keyboard_layout.h @@ -0,0 +1,113 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016, 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 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 updateNotifier(); + 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..27df133 --- /dev/null +++ b/keyboard_layout_switching.cpp @@ -0,0 +1,258 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#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) + : QObject(layout) + , 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) +{ + m_xkb->switchToLayout(layout); +} + +quint32 Policy::layout() const +{ + return m_xkb->currentLayout(); +} + +Policy *Policy::create(Xkb *xkb, KeyboardLayout *layout, const QString &policy) +{ + if (policy.toLower() == QStringLiteral("desktop")) { + return new VirtualDesktopPolicy(xkb, layout); + } + if (policy.toLower() == QStringLiteral("window")) { + return new WindowPolicy(xkb, layout); + } + if (policy.toLower() == QStringLiteral("winclass")) { + return new ApplicationPolicy(xkb, layout); + } + return new GlobalPolicy(xkb, layout); +} + +GlobalPolicy::GlobalPolicy(Xkb *xkb, KeyboardLayout *layout) + : Policy(xkb, layout) +{ +} + +GlobalPolicy::~GlobalPolicy() = default; + +VirtualDesktopPolicy::VirtualDesktopPolicy(Xkb *xkb, KeyboardLayout *layout) + : Policy(xkb, layout) +{ + connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, this, &VirtualDesktopPolicy::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) + : Policy(xkb, layout) +{ + connect(workspace(), &Workspace::clientActivated, this, &ApplicationPolicy::clientActivated); +} + +ApplicationPolicy::~ApplicationPolicy() +{ +} + +void ApplicationPolicy::clientActivated(AbstractClient *c) +{ + if (!c) { + return; + } + // ignore some special types + if (c->isDesktop() || c->isDock()) { + return; + } + quint32 layout = 0; + for (auto it = m_layouts.constBegin(); it != m_layouts.constEnd(); it++) { + if (AbstractClient::belongToSameApplication(c, it.key())) { + layout = it.value(); + break; + } + } + setLayout(layout); +} + +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..4e21fee --- /dev/null +++ b/keyboard_layout_switching.h @@ -0,0 +1,138 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_KEYBOARD_LAYOUT_SWITCHING_H +#define KWIN_KEYBOARD_LAYOUT_SWITCHING_H + +#include +#include + +namespace KWin +{ + +class AbstractClient; +class KeyboardLayout; +class Xkb; +class VirtualDesktop; + +namespace KeyboardLayoutSwitching +{ + +class Policy : public QObject +{ + Q_OBJECT +public: + virtual ~Policy(); + + virtual QString name() const = 0; + + static Policy *create(Xkb *xkb, KeyboardLayout *layout, const QString &policy); + +protected: + explicit Policy(Xkb *xkb, KeyboardLayout *layout); + virtual void clearCache() = 0; + virtual void layoutChanged() = 0; + + void setLayout(quint32 layout); + quint32 layout() const; + +private: + Xkb *m_xkb; + KeyboardLayout *m_layout; +}; + +class GlobalPolicy : public Policy +{ + Q_OBJECT +public: + explicit GlobalPolicy(Xkb *xkb, KeyboardLayout *layout); + ~GlobalPolicy() override; + + QString name() const override { + return QStringLiteral("Global"); + } + +protected: + void clearCache() override {} + void layoutChanged() override {} +}; + +class VirtualDesktopPolicy : public Policy +{ + Q_OBJECT +public: + explicit VirtualDesktopPolicy(Xkb *xkb, KeyboardLayout *layout); + ~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); + ~ApplicationPolicy() override; + + QString name() const override { + return QStringLiteral("WinClass"); + } + +protected: + void clearCache() override; + void layoutChanged() override; + +private: + void clientActivated(AbstractClient *c); + QHash m_layouts; +}; + +} +} + +#endif diff --git a/keyboard_repeat.cpp b/keyboard_repeat.cpp new file mode 100644 index 0000000..bdb1195 --- /dev/null +++ b/keyboard_repeat.cpp @@ -0,0 +1,73 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016, 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#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) + , 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..6e9156e --- /dev/null +++ b/keyboard_repeat.h @@ -0,0 +1,56 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016, 2017 Martin Gräßlin + +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, see . +*********************************************************************/ +#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; +}; + + +} + +#endif diff --git a/killwindow.cpp b/killwindow.cpp new file mode 100644 index 0000000..bf4f8ea --- /dev/null +++ b/killwindow.cpp @@ -0,0 +1,61 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..9c00b32 --- /dev/null +++ b/killwindow.h @@ -0,0 +1,41 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak +Copyright (C) 2012 Martin Gräßlin + +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, see . +*********************************************************************/ + +#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..9bbf81a --- /dev/null +++ b/kwin.kcfg @@ -0,0 +1,307 @@ + + + + + + Switch to Window Tab to the Left/Right + + + Alt + + + Nothing + + + Raise + + + Start Window Tab Drag + + + Operations menu + + + Activate and raise + + + Start Window Tab Drag + + + 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 + + + false + + + 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 + + + 3 + 0 + 6 + + + glx + + + true + + + + + true + + + 90 + + + 1 + + + 1 + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + true + + + true + + + true + + + thumbnails + + + diff --git a/kwin.notifyrc b/kwin.notifyrc new file mode 100644 index 0000000..246720a --- /dev/null +++ b/kwin.notifyrc @@ -0,0 +1,285 @@ +[Global] +IconName=kwin +Comment=KWin Window Manager +Comment[ar]=مدير النوافذ كوين +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 Jendela 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ų tvarkyklė +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[tg]=Мудири тирезаҳои KWin +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[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]=Compositing has been suspended +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 buvo sustabdytas +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[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 de 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]=Outro aplicativo 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šė sustabdyti 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[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]=Graphics Reset +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[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]=Ocorreu 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]=Peristiwa atur ulang grafik terjadi +Comment[it]=Si è verificato un evento di azzeramento della grafica +Comment[kk]=Графиканы ысыру оқиғасы болды +Comment[ko]=그래픽 초기화 이벤트가 발생함 +Comment[lt]=Įvyko grafikos pirminės būsenos atkūrimo veiksmas +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 diff --git a/kwinbindings.cpp b/kwinbindings.cpp new file mode 100644 index 0000000..40e98f0 --- /dev/null +++ b/kwinbindings.cpp @@ -0,0 +1,169 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ + +// 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("Walk Through Window Tabs"), 0, slotActivateNextTab); +DEF(I18N_NOOP("Walk Through Window Tabs (Reverse)"), 0, slotActivatePrevTab); +DEF(I18N_NOOP("Remove Window From Group"), 0, slotUntab); + +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"), 0, 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::slotToggleCompositing); +DEF6(I18N_NOOP("Invert Screen Colors"), 0, kwinApp()->platform(), Platform::invertScreen); + +#undef DEF +#undef DEF2 +#undef DEF3 + +// } diff --git a/lanczosfilter.cpp b/lanczosfilter.cpp new file mode 100644 index 0000000..fbd4a39 --- /dev/null +++ b/lanczosfilter.cpp @@ -0,0 +1,426 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2010 by Fredrik Höglund +Copyright (C) 2010 Martin Gräßlin + +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, see . +*********************************************************************/ + +#include "lanczosfilter.h" +#include "client.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 + +namespace KWin +{ + +LanczosFilter::LanczosFilter(QObject* parent) + : QObject(parent) + , m_offscreenTex(0) + , m_offscreenTarget(0) + , m_inited(false) + , m_shader(0) + , m_uOffsets(0) + , m_uKernel(0) +{ +} + +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_CORE) << "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(":/resources/shaders/1.40/lanczos-fragment.glsl") : + QStringLiteral(":/resources/shaders/1.10/lanczos-fragment.glsl")); + if (!ff.open(QIODevice::ReadOnly)) { + qCDebug(KWIN_CORE) << "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_CORE) << "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; + } + + memset(m_kernel, 0, 16 * sizeof(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) +{ + memset(m_offsets, 0, 16 * sizeof(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 = 0; + 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(); + + delete m_offscreenTarget; + delete m_offscreenTex; + m_offscreenTarget = 0; + m_offscreenTex = 0; + foreach (Client *c, Workspace::self()->clientList()) { + discardCacheTexture(c->effectWindow()); + } + foreach (Client *c, Workspace::self()->desktopList()) { + discardCacheTexture(c->effectWindow()); + } + foreach (Unmanaged *u, Workspace::self()->unmanagedList()) { + discardCacheTexture(u->effectWindow()); + } + foreach (Deleted *d, Workspace::self()->deletedList()) { + discardCacheTexture(d->effectWindow()); + } + } +} + +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, 16, (const GLfloat*)m_offsets); + glUniform4fv(m_uKernel, 16, (const GLfloat*)m_kernel); +} + +} // namespace + diff --git a/lanczosfilter.h b/lanczosfilter.h new file mode 100644 index 0000000..90dff42 --- /dev/null +++ b/lanczosfilter.h @@ -0,0 +1,77 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2010 by Fredrik Höglund +Copyright (C) 2010 Martin Gräßlin + +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, see . +*********************************************************************/ + +#ifndef KWIN_LANCZOSFILTER_P_H +#define KWIN_LANCZOSFILTER_P_H + +#include +#include +#include +#include +#include + +#include +#include + +namespace KWin +{ + +class EffectWindow; +class EffectWindowImpl; +class WindowPaintData; +class GLTexture; +class GLRenderTarget; +class GLShader; + +class KWIN_EXPORT LanczosFilter + : public QObject +{ + Q_OBJECT + +public: + explicit LanczosFilter(QObject* parent = 0); + ~LanczosFilter(); + void performPaint(EffectWindowImpl* w, int mask, QRegion region, WindowPaintData& data); + +protected: + virtual void timerEvent(QTimerEvent*); +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; + QVector2D m_offsets[16]; + QVector4D m_kernel[16]; +}; + +} // namespace + +#endif // KWIN_LANCZOSFILTER_P_H diff --git a/layers.cpp b/layers.cpp new file mode 100644 index 0000000..caa7db1 --- /dev/null +++ b/layers.cpp @@ -0,0 +1,822 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 1999, 2000 Matthias Ettrich +Copyright (C) 2003 Lubos Lunak + +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, see . +*********************************************************************/ + +// 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 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 and OnScreenDisplayLayer. + The NoficationLayer contains notification windows which are kept above all windows except the active + fullscreen window. 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 ClientList 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 + +#include "utils.h" +#include "client.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 "shell_client.h" +#include "wayland_server.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; + } + ToplevelList 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); + 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.reserve(newWindowStack.size() + 2*stacking_order.size()); // *2 for inputWindow + + for (int i = stacking_order.size() - 1; i >= 0; --i) { + Client *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) { + Client *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? + 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[ desktops.count() + clients.count()]; + // TODO this is still not completely in the map order + for (ClientList::ConstIterator it = desktops.constBegin(); it != desktops.constEnd(); ++it) + cl[pos++] = (*it)->window(); + for (ClientList::ConstIterator it = clients.constBegin(); it != clients.constEnd(); ++it) + cl[pos++] = (*it)->window(); + rootInfo()->setClientList(cl, pos); + delete [] cl; + } + + cl = new xcb_window_t[ stacking_order.count()]; + pos = 0; + for (ToplevelList::ConstIterator it = stacking_order.constBegin(); it != stacking_order.constEnd(); ++it) { + if ((*it)->isClient()) + cl[pos++] = (*it)->window(); + } + rootInfo()->setClientListStacking(cl, pos); + delete [] cl; + + // Make the cached stacking order invalid here, in case we need the new stacking order before we get + // the matching event, due to X being asynchronous. + markXStackingOrderAsDirty(); +} + +/*! + 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 ); + ToplevelList 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 0; +} + +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 NULL; +} + +void Workspace::raiseOrLowerClient(AbstractClient *c) +{ + if (!c) return; + AbstractClient* topmost = NULL; +// 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 + ClientList wins; + if (Client *client = dynamic_cast(c)) { + wins = ensureStackingOrder(client->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 = 0; +} + +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 (ToplevelList::Iterator 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::Client *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) +{ + 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) ? 0 : other; + break; + } + } + } + if (under) { + unconstrained_stacking_order.removeAll(c); + unconstrained_stacking_order.insert(unconstrained_stacking_order.indexOf(under), c); + } + + 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(Client* c) +{ + if (c->sessionStackingOrder() < 0) + return; + StackingUpdatesBlocker blocker(this); + unconstrained_stacking_order.removeAll(c); + for (ToplevelList::Iterator it = unconstrained_stacking_order.begin(); // from bottom + it != unconstrained_stacking_order.end(); + ++it) { + Client *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. + */ +ToplevelList Workspace::constrainedStackingOrder() +{ + ToplevelList layer[ NumLayers ]; + + // build the order from layers + QVector< QMap > minimum_layer(screens()->count()); + for (ToplevelList::ConstIterator it = unconstrained_stacking_order.constBegin(), + end = unconstrained_stacking_order.constEnd(); it != end; ++it) { + Layer l = (*it)->layer(); + + const int screen = (*it)->screen(); + Client *c = qobject_cast(*it); + QMap< Group*, Layer >::iterator mLayer = minimum_layer[screen].find(c ? c->group() : NULL); + 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].insertMulti(c->group(), l); + } + layer[ l ].append(*it); + } + ToplevelList stacking; + for (Layer 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; + ) { + AbstractClient *current = qobject_cast(stacking[i]); + if (!current || !current->isTransient()) { + --i; + continue; + } + int i2 = -1; + Client *ccurrent = qobject_cast(current); + if (ccurrent && ccurrent->groupTransient()) { + if (ccurrent->group()->members().count() > 0) { + // find topmost client this one is transient for + for (i2 = stacking.size() - 1; + i2 >= 0; + --i2) { + if (stacking[ i2 ] == stacking[ i ]) { + i2 = -1; // don't reorder, already the topmost in the group + break; + } + AbstractClient *c2 = qobject_cast(stacking[ i2 ]); + if (!c2) { + continue; + } + if (c2->hasTransient(current, true) + && keepTransientAbove(c2, current)) + break; + } + } // else i2 remains pointing at -1 + } else { + for (i2 = stacking.size() - 1; + i2 >= 0; + --i2) { + AbstractClient *c2 = qobject_cast(stacking[ i2 ]); + if (!c2) { + continue; + } + if (c2 == current) { + i2 = -1; // don't reorder, already on top of its mainwindow + break; + } + if (c2 == current->transientFor() + && keepTransientAbove(c2, current)) + break; + } + } + if (i2 == -1) { + --i; + continue; + } + stacking.removeAt(i); + --i; // move onto the next item (for next for () iteration) + --i2; // adjust index of the mainwindow after the remove above + if (!current->transients().isEmpty()) // 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 ToplevelList &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 +ClientList Workspace::ensureStackingOrder(const ClientList& 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 (const Client *ct = dynamic_cast(transient)) { + if (ct->isDialog() && !ct->isModal() && ct->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; +} + +// Returns all windows in their stacking order on the root window. +ToplevelList 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 = unmanaged.count(); + for (unsigned int i = 0; + i < count; + ++i) { + for (auto it = unmanaged.constBegin(); it != unmanaged.constEnd(); ++it) { + Unmanaged *u = *it; + if (u->window() == windows[i]) { + x_stacking.append(u); + foundUnmanagedCount--; + break; + } + } + if (foundUnmanagedCount == 0) { + break; + } + } + } + if (waylandServer()) { + const auto clients = waylandServer()->internalClients(); + for (auto c: clients) { + if (c->isShown(false)) { + x_stacking << c; + } + } + } + m_xStackingDirty = false; +} + +//******************************* +// Client +//******************************* + +void Client::restackWindow(xcb_window_t above, int detail, NET::RequestSource src, xcb_timestamp_t timestamp, bool send_event) +{ + Client *other = 0; + if (detail == XCB_STACK_MODE_OPPOSITE) { + other = workspace()->findClient(Predicate::WindowMatch, above); + if (!other) { + workspace()->raiseOrLowerClient(this); + return; + } + ToplevelList::const_iterator 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->geometry().intersects(geometry())) + workspace()->raiseClientRequest(this, src, timestamp); + return; + } + else if (detail == XCB_STACK_MODE_BOTTOM_IF) { + other = workspace()->findClient(Predicate::WindowMatch, above); + if (other && other->geometry().intersects(geometry())) + workspace()->lowerClientRequest(this, src, timestamp); + return; + } + + if (!other) + other = workspace()->findClient(Predicate::WindowMatch, above); + + if (other && detail == XCB_STACK_MODE_ABOVE) { + ToplevelList::const_iterator 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; + } + Client *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 = 0; + } + + 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(); +} + +void Client::doSetKeepAbove() +{ + // Update states of all other windows in this group + if (tabGroup()) + tabGroup()->updateStates(this, TabGroup::Layer); +} + +void Client::doSetKeepBelow() +{ + // Update states of all other windows in this group + if (tabGroup()) + tabGroup()->updateStates(this, TabGroup::Layer); +} + +bool Client::belongsToDesktop() const +{ + foreach (const Client *c, group()->members()) { + if (c->isDesktop()) + return true; + } + return false; +} + +bool rec_checkTransientOnTop(const QList &transients, const Client *topmost) +{ + foreach (const AbstractClient *transient, transients) { + if (transient == topmost || rec_checkTransientOnTop(transient->transients(), topmost)) { + return true; + } + } + return false; +} + +} // namespace diff --git a/libinput/connection.cpp b/libinput/connection.cpp new file mode 100644 index 0000000..7664659 --- /dev/null +++ b/libinput/connection.cpp @@ -0,0 +1,685 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#include "connection.h" +#include "context.h" +#include "device.h" +#include "events.h" +#ifndef KWIN_BUILD_TESTING +#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() { + 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(); + } +} + +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()); + struct Axis { + qreal delta = 0.0; + quint32 time = 0; + }; + QMap deltas; + auto update = [&deltas] (PointerEvent *pe) { + const auto axis = pe->axis(); + for (auto it = axis.begin(); it != axis.end(); ++it) { + deltas[*it].delta += pe->axisValue(*it); + deltas[*it].time = pe->time(); + } + }; + update(pe); + auto it = m_eventQueue.begin(); + while (it != m_eventQueue.end()) { + if ((*it)->type() == LIBINPUT_EVENT_POINTER_AXIS) { + QScopedPointer p(static_cast(*it)); + update(p.data()); + it = m_eventQueue.erase(it); + } else { + break; + } + } + for (auto it = deltas.constBegin(); it != deltas.constEnd(); ++it) { + emit pointerAxisChanged(it.key(), it.value().delta, it.value().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 &geo = screens()->geometry(te->device()->screenId()); + emit touchDown(te->id(), geo.topLeft() + te->absolutePos(geo.size()), 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 &geo = screens()->geometry(te->device()->screenId()); + emit touchMotion(te->id(), geo.topLeft() + te->absolutePos(geo.size()), 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; + } + 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); + 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..44495ed --- /dev/null +++ b/libinput/connection.h @@ -0,0 +1,172 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 Connection : public QObject +{ + Q_OBJECT + +public: + ~Connection(); + + 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, 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 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..dd11619 --- /dev/null +++ b/libinput/context.cpp @@ -0,0 +1,184 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..b4843f4 --- /dev/null +++ b/libinput/context.h @@ -0,0 +1,80 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..330499d --- /dev/null +++ b/libinput/device.cpp @@ -0,0 +1,500 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#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 +}; + +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; } + + 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; +}; + +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)} +}; + +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)) +#if 0 + // next libinput version + , m_tabletPad(libinput_device_has_capability(m_device, LIBINPUT_DEVICE_CAP_TABLET_PAD)) +#else + , m_tabletPad(false) +#endif + , 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_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{}) +{ + 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, ""); + }; + + 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::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(); + } + } +} + +#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::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..dcd173f --- /dev/null +++ b/libinput/device.h @@ -0,0 +1,537 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2016 Martin Gräßlin + +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, see . +*********************************************************************/ +#ifndef KWIN_LIBINPUT_DEVICE_H +#define KWIN_LIBINPUT_DEVICE_H + +#include + +#include + +#include +#include +#include +#include + +struct libinput_device; + +namespace KWin +{ +namespace LibInput +{ +enum class ConfigKey; + +class 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(Qt::MouseButtons 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) + + // switches + Q_PROPERTY(bool switchDevice READ isSwitch CONSTANT) + Q_PROPERTY(bool lidSwitch READ isLidSwitch CONSTANT) + Q_PROPERTY(bool tabletModeSwitch READ isTabletModeSwitch CONSTANT) + + +public: + explicit Device(libinput_device *device, QObject *parent = nullptr); + virtual ~Device(); + + 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); + + 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 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 @link 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; + } + + /** + * 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(); + +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; + 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; + + 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..3f79062 --- /dev/null +++ b/libinput/events.cpp @@ -0,0 +1,330 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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_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); +} + +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); +} + +} +} diff --git a/libinput/events.h b/libinput/events.h new file mode 100644 index 0000000..c16fae2 --- /dev/null +++ b/libinput/events.h @@ -0,0 +1,201 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2014 Martin Gräßlin + +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, see . +*********************************************************************/ +#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); + virtual ~KeyEvent(); + + 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); + virtual ~PointerEvent(); + + 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; + + 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); + virtual ~TouchEvent(); + + 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: + virtual ~GestureEvent(); + + 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); + virtual ~PinchGestureEvent(); + + qreal scale() const; + qreal angleDelta() const; +}; + +class SwipeGestureEvent : public GestureEvent +{ +public: + SwipeGestureEvent(libinput_event *event, libinput_event_type type); + virtual ~SwipeGestureEvent(); +}; + +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; +}; + +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..8a7c2f2 --- /dev/null +++ b/libinput/libinput_logging.cpp @@ -0,0 +1,21 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..a91b52d --- /dev/null +++ b/libinput/libinput_logging.h @@ -0,0 +1,26 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright (C) 2015 Martin Gräßlin + +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, see . +*********************************************************************/ +#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..9771719 --- /dev/null +++ b/libkwineffects/CMakeLists.txt @@ -0,0 +1,120 @@ +########### 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 11 +) + +### 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 + XCB::XCB + XCB::XFIXES + XCB::RENDER + KF5::WaylandServer + ) + +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 + kwineffects.cpp + anidata.cpp + kwinanimationeffect.cpp + logging.cpp + ) + +set(kwineffects_QT_LIBS + Qt5::DBus + Qt5::Widgets +) + +set(kwineffects_KDE_LIBS + KF5::ConfigCore + KF5::CoreAddons + KF5::WindowSystem +) + +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} +) +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 + kwinglutils.cpp + kwingltexture.cpp + kwinglutils_funcs.cpp + kwinglplatform.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 + kwinglobals.h + kwineffects.h + kwinanimationeffect.h + kwinglplatform.h + kwinglutils.h + kwinglutils_funcs.h + kwingltexture.h + kwinxrenderutils.h + ${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 + 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 + +

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

;(=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" + +#: kcm.cpp:100 +#, kde-format +msgid "Remember settings separately for every window" +msgstr "Herroep instellings appart vir elke venster" + +#: kcm.cpp:101 +#, kde-format +msgid "Show internal settings for remembering" +msgstr "Wys interne instellings vir herroeping" + +#: kcm.cpp:102 +#, kde-format +msgid "Internal setting for remembering" +msgstr "Interne instelling vir herroeping" + +#: main.cpp:150 +#, kde-format +msgid "Application settings for %1" +msgstr "Toepassings instellings vir %1" + +#: main.cpp:170 +#, kde-format +msgid "Window settings for %1" +msgstr "Venster instellings vir %1" + +#: main.cpp:222 +#, 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:253 +#, kde-format +msgid "KWin" +msgstr "KWin" + +#: main.cpp:261 +#, kde-format +msgid "KWin helper utility" +msgstr "KWin helper nutsprogramme" + +#: main.cpp:262 +#, kde-format +msgid "WId of the window for special window settings." +msgstr "" + +#: main.cpp:263 +#, kde-format +msgid "Whether the settings should affect all windows of the application." +msgstr "" + +#: main.cpp:271 +#, 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" + +#: ruleslist.cpp:153 +#, kde-format +msgid "Export Rules" +msgstr "" + +#: ruleslist.cpp:154 ruleslist.cpp:166 +#, kde-format +msgid "KWin Rules (*.kwinrule)" +msgstr "" + +#: ruleslist.cpp:165 +#, kde-format +msgid "Import Rules" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, new_button) +#: ruleslist.ui:32 +#, kde-format +msgid "&New..." +msgstr "Nuwe..." + +#. i18n: ectx: property (text), widget (QPushButton, modify_button) +#: ruleslist.ui:39 +#, kde-format +msgid "&Modify..." +msgstr "Pas aan..." + +#. i18n: ectx: property (text), widget (QPushButton, delete_button) +#: ruleslist.ui:46 +#, fuzzy, kde-format +msgid "Delete" +msgstr "Ondersoek" + +#. i18n: ectx: property (text), widget (QPushButton, moveup_button) +#: ruleslist.ui:56 +#, kde-format +msgid "Move &Up" +msgstr "Skuif Op" + +#. i18n: ectx: property (text), widget (QPushButton, movedown_button) +#: ruleslist.ui:63 +#, kde-format +msgid "Move &Down" +msgstr "Skuif Af" + +#. i18n: ectx: property (text), widget (QPushButton, import_button) +#: ruleslist.ui:100 +#, kde-format +msgid "&Import" +msgstr "" + +#. i18n: ectx: property (text), widget (QPushButton, export_button) +#: ruleslist.ui:107 +#, kde-format +msgid "&Export" +msgstr "" + +#: ruleswidget.cpp:68 +#, kde-format +msgid "" +"Enable this checkbox to alter this window property for the specified " +"window(s)." +msgstr "" +"Stel hierdie opsieboks in werking om hierdie venster eienskap te verander " +"vir die gespesifiseerde venster(s)." + +#: ruleswidget.cpp:70 +#, fuzzy, kde-format +#| msgid "" +#| "Specify how the window property should be affected:

  • Do Not " +#| "Affect: The window property will not be affected and therefore the " +#| "default handling for it will be used. Specifying this will block more " +#| "generic window settings from taking effect.
  • Apply Initially: The window property will be only set to the given value after the " +#| "window is created. No further changes will be affected.
  • Remember: The value of the window property will be " +#| "remembered and every time time the window is created, the last remembered " +#| "value will be applied.
  • Force: The window property will " +#| "be always forced to the given value.
  • Apply Now: The " +#| "window property will be set to the given value immediately and will not " +#| "be affected later (this action will be deleted afterwards).
  • Force temporarily: The window property will be forced to " +#| "the given value until it is hidden (this action will be deleted after the " +#| "window is hidden).
" +msgid "" +"Specify how the window property should be affected:
  • Do Not Affect:" +" The window property will not be affected and therefore the default " +"handling for it will be used. Specifying this will block more generic window " +"settings from taking effect.
  • Apply Initially: The window " +"property will be only set to the given value after the window is created. No " +"further changes will be affected.
  • Remember: The value of " +"the window property will be remembered and every time the window is created, " +"the last remembered value will be applied.
  • Force: The " +"window property will be always forced to the given value.
  • Apply " +"Now: The window property will be set to the given value immediately and " +"will not be affected later (this action will be deleted afterwards).
  • Force temporarily: The window property will be forced to the " +"given value until it is hidden (this action will be deleted after the window " +"is hidden).
" +msgstr "" +"Spesifiseer hoe die venster eienskap geaffekteer behoort te word:" +"

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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 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≀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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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