diff --git a/CMakeLists.txt b/CMakeLists.txt index b86bece6e..e46f46608 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,793 +1,794 @@ cmake_minimum_required(VERSION 3.1 FATAL_ERROR) project(KWin) set(PROJECT_VERSION "5.19.80") set(PROJECT_VERSION_MAJOR 5) set(QT_MIN_VERSION "5.14.0") set(KF5_MIN_VERSION "5.70.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 Script Sensors UiTools Widgets X11Extras ) find_package(Qt5Test ${QT_MIN_VERSION} CONFIG QUIET) set_package_properties(Qt5Test PROPERTIES PURPOSE "Required for tests" TYPE OPTIONAL ) add_feature_info("Qt5Test" Qt5Test_FOUND "Required for building tests") if (NOT Qt5Test_FOUND) set(BUILD_TESTING OFF CACHE BOOL "Build the testing tree.") endif() include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(KDEClangFormat) include(ECMInstallIcons) include(ECMOptionalAddSubdirectory) add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0 -DQT_USE_QSTRINGBUILDER -DQT_NO_URL_CAST_FROM_STRING) set(CMAKE_CXX_STANDARD 14) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt5Multimedia QUIET) set_package_properties(Qt5Multimedia PROPERTIES PURPOSE "Runtime-only dependency for effect video playback" TYPE RUNTIME ) # required frameworks by Core find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Config ConfigWidgets CoreAddons Crash GlobalAccel I18n IconThemes IdleTime Notifications Package Plasma Wayland WidgetsAddons WindowSystem ) # required frameworks by config modules find_package(KF5 ${KF5_MIN_VERSION} REQUIRED COMPONENTS Completion Declarative KCMUtils KIO NewStuff Service TextWidgets XmlGui ) find_package(Threads) set_package_properties(Threads PROPERTIES PURPOSE "Needed for VirtualTerminal support in KWin Wayland" TYPE REQUIRED ) # optional frameworks find_package(KF5Activities ${KF5_MIN_VERSION} CONFIG) set_package_properties(KF5Activities PROPERTIES PURPOSE "Enable building of KWin with kactivities support" TYPE OPTIONAL ) add_feature_info("KF5Activities" KF5Activities_FOUND "Enable building of KWin with kactivities support") find_package(KF5DocTools ${KF5_MIN_VERSION} CONFIG) set_package_properties(KF5DocTools PROPERTIES PURPOSE "Enable building documentation" TYPE OPTIONAL ) add_feature_info("KF5DocTools" KF5DocTools_FOUND "Enable building documentation") find_package(KF5Kirigami2 ${KF5_MIN_VERSION} CONFIG) set_package_properties(KF5Kirigami2 PROPERTIES DESCRIPTION "A QtQuick based components set" PURPOSE "Required at runtime for Virtual desktop KCM and the virtual keyboard" TYPE RUNTIME ) find_package(KDecoration2 5.18.0 CONFIG REQUIRED) find_package(KScreenLocker CONFIG REQUIRED) set_package_properties(KScreenLocker PROPERTIES TYPE REQUIRED PURPOSE "For screenlocker integration in kwin_wayland" ) find_package(KWaylandServer CONFIG REQUIRED) set_package_properties(KWaylandServer PROPERTIES TYPE REQUIRED PURPOSE "For Wayland integration" ) find_package(Breeze 5.9.0 CONFIG) set_package_properties(Breeze PROPERTIES TYPE OPTIONAL PURPOSE "For setting the default window decoration plugin" ) if (${Breeze_FOUND}) if (${BREEZE_WITH_KDECORATION}) set(HAVE_BREEZE_DECO true) else() set(HAVE_BREEZE_DECO FALSE) endif() else() set(HAVE_BREEZE_DECO FALSE) endif() add_feature_info("Breeze-Decoration" HAVE_BREEZE_DECO "Default decoration plugin Breeze") find_package(EGL) set_package_properties(EGL PROPERTIES TYPE RUNTIME PURPOSE "Required to build KWin with EGL support" ) find_package(epoxy) set_package_properties(epoxy PROPERTIES DESCRIPTION "libepoxy" URL "https://github.com/anholt/libepoxy" TYPE REQUIRED PURPOSE "OpenGL dispatch library" ) set(HAVE_DL_LIBRARY FALSE) if (epoxy_HAS_GLX) find_library(DL_LIBRARY dl) if (DL_LIBRARY) set(HAVE_DL_LIBRARY TRUE) endif() endif() find_package(Wayland 1.2 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 "https://www.freedesktop.org/wiki/Software/systemd/" DESCRIPTION "Linux device library." TYPE REQUIRED PURPOSE "Required for input handling on Wayland." ) find_package(Libdrm 2.4.62) set_package_properties(Libdrm PROPERTIES TYPE OPTIONAL PURPOSE "Required for drm output on Wayland.") set(HAVE_DRM FALSE) if (Libdrm_FOUND) set(HAVE_DRM TRUE) endif() find_package(gbm) set_package_properties(gbm PROPERTIES TYPE OPTIONAL PURPOSE "Required for egl output of drm backend.") set(HAVE_GBM FALSE) if (HAVE_DRM AND gbm_FOUND) set(HAVE_GBM TRUE) endif() option(KWIN_BUILD_EGL_STREAM_BACKEND "Enable building of EGLStream based DRM backend" ON) if (HAVE_DRM AND KWIN_BUILD_EGL_STREAM_BACKEND) set(HAVE_EGL_STREAMS TRUE) endif() find_package(libhybris) set_package_properties(libhybris PROPERTIES TYPE OPTIONAL PURPOSE "Required for libhybris backend") set(HAVE_LIBHYBRIS ${libhybris_FOUND}) find_package(X11) set_package_properties(X11 PROPERTIES DESCRIPTION "X11 libraries" URL "https://www.x.org" TYPE REQUIRED ) add_feature_info("XInput" X11_Xinput_FOUND "Required for poll-free mouse cursor updates") set(HAVE_X11_XINPUT ${X11_Xinput_FOUND}) # All the required XCB components find_package(XCB 1.10 REQUIRED COMPONENTS COMPOSITE CURSOR DAMAGE GLX ICCCM IMAGE KEYSYMS RANDR RENDER SHAPE SHM SYNC XCB XFIXES ) set_package_properties(XCB PROPERTIES TYPE REQUIRED) # and the optional XCB dependencies if (XCB_ICCCM_VERSION VERSION_LESS "0.4") set(XCB_ICCCM_FOUND FALSE) endif() add_feature_info("XCB-ICCCM" XCB_ICCCM_FOUND "Required for building test applications for KWin") find_package(X11_XCB) set_package_properties(X11_XCB PROPERTIES PURPOSE "Required for building X11 windowed backend of kwin_wayland" TYPE OPTIONAL ) # dependencies for QPA plugin find_package(Qt5FontDatabaseSupport REQUIRED) find_package(Qt5ThemeSupport REQUIRED) find_package(Qt5EventDispatcherSupport REQUIRED) find_package(Freetype REQUIRED) set_package_properties(Freetype PROPERTIES DESCRIPTION "A font rendering engine" URL "https://www.freetype.org" TYPE REQUIRED PURPOSE "Needed for KWin's QPA plugin." ) find_package(Fontconfig REQUIRED) set_package_properties(Fontconfig PROPERTIES TYPE REQUIRED PURPOSE "Needed for KWin's QPA plugin." ) find_package(Xwayland) set_package_properties(Xwayland PROPERTIES URL "https://x.org" DESCRIPTION "Xwayland X server" TYPE RUNTIME PURPOSE "Needed for running kwin_wayland" ) find_package(Libcap) set_package_properties(Libcap PROPERTIES TYPE OPTIONAL PURPOSE "Needed for running kwin_wayland with real-time scheduling policy" ) set(HAVE_LIBCAP ${Libcap_FOUND}) find_package(hwdata) set_package_properties(hwdata PROPERTIES TYPE RUNTIME PURPOSE "Runtime-only dependency needed for mapping monitor hardware vendor IDs to full names" URL "https://github.com/vcrhonek/hwdata" ) find_package(QAccessibilityClient CONFIG) set_package_properties(QAccessibilityClient PROPERTIES URL "https://www.kde.org" DESCRIPTION "KDE client-side accessibility library" TYPE OPTIONAL PURPOSE "Required to enable accessibility features" ) set(HAVE_ACCESSIBILITY ${QAccessibilityClient_FOUND}) include(ECMQMLModules) ecm_find_qmlmodule(QtQuick 2.3) ecm_find_qmlmodule(QtQuick.Controls 1.2) ecm_find_qmlmodule(QtQuick.Layouts 1.3) ecm_find_qmlmodule(QtQuick.VirtualKeyboard 2.1) ecm_find_qmlmodule(QtQuick.Window 2.1) ecm_find_qmlmodule(QtMultimedia 5.0) ecm_find_qmlmodule(org.kde.kquickcontrolsaddons 2.0) ecm_find_qmlmodule(org.kde.plasma.core 2.0) ecm_find_qmlmodule(org.kde.plasma.components 2.0) ########### configure tests ############### include(CMakeDependentOption) option(KWIN_BUILD_DECORATIONS "Enable building of KWin decorations." ON) option(KWIN_BUILD_KCMS "Enable building of KWin configuration modules." ON) option(KWIN_BUILD_TABBOX "Enable building of KWin Tabbox functionality" ON) option(KWIN_BUILD_XRENDER_COMPOSITING "Enable building of KWin with XRender Compositing support" ON) cmake_dependent_option(KWIN_BUILD_ACTIVITIES "Enable building of KWin with kactivities support" ON "KF5Activities_FOUND" OFF) # Binary name of KWin set(KWIN_NAME "kwin") set(KWIN_INTERNAL_NAME_X11 "kwin_x11") set(KWIN_INTERNAL_NAME_WAYLAND "kwin_wayland") # KWIN_HAVE_XRENDER_COMPOSITING - whether XRender-based compositing support is available: may be disabled if (KWIN_BUILD_XRENDER_COMPOSITING) set(KWIN_HAVE_XRENDER_COMPOSITING 1) endif() include_directories(${XKB_INCLUDE_DIR}) include_directories(${epoxy_INCLUDE_DIR}) set(HAVE_EPOXY_GLX ${epoxy_HAS_GLX}) # for things that are also used by kwin libraries configure_file(libkwineffects/kwinconfig.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/libkwineffects/kwinconfig.h ) # for kwin internal things set(HAVE_X11_XCB ${X11_XCB_FOUND}) include(CheckIncludeFile) include(CheckIncludeFiles) include(CheckSymbolExists) check_include_files(unistd.h HAVE_UNISTD_H) check_include_files(malloc.h HAVE_MALLOC_H) check_include_file("sys/prctl.h" HAVE_SYS_PRCTL_H) check_symbol_exists(PR_SET_DUMPABLE "sys/prctl.h" HAVE_PR_SET_DUMPABLE) check_symbol_exists(PR_SET_PDEATHSIG "sys/prctl.h" HAVE_PR_SET_PDEATHSIG) check_include_file("sys/procctl.h" HAVE_SYS_PROCCTL_H) check_symbol_exists(PROC_TRACE_CTL "sys/procctl.h" HAVE_PROC_TRACE_CTL) if (HAVE_PR_SET_DUMPABLE OR HAVE_PROC_TRACE_CTL) set(CAN_DISABLE_PTRACE TRUE) endif() add_feature_info("prctl/procctl tracing control" CAN_DISABLE_PTRACE "Required for disallowing ptrace on kwin_wayland process") check_include_file("sys/sysmacros.h" HAVE_SYS_SYSMACROS_H) check_include_file("linux/vt.h" HAVE_LINUX_VT_H) add_feature_info("linux/vt.h" HAVE_LINUX_VT_H "Required for virtual terminal support under wayland") check_include_file("linux/fb.h" HAVE_LINUX_FB_H) add_feature_info("linux/fb.h" HAVE_LINUX_FB_H "Required for the fbdev backend") check_symbol_exists(SCHED_RESET_ON_FORK "sched.h" HAVE_SCHED_RESET_ON_FORK) add_feature_info("SCHED_RESET_ON_FORK" HAVE_SCHED_RESET_ON_FORK "Required for running kwin_wayland with real-time scheduling") configure_file(config-kwin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-kwin.h) ########### global ############### set(kwin_effects_dbus_xml ${CMAKE_CURRENT_SOURCE_DIR}/org.kde.kwin.Effects.xml) qt5_add_dbus_interface(effects_interface_SRCS ${kwin_effects_dbus_xml} kwineffects_interface) add_library(KWinEffectsInterface STATIC ${effects_interface_SRCS}) target_link_libraries(KWinEffectsInterface Qt5::DBus) include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}/libkwineffects ${CMAKE_CURRENT_BINARY_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/libkwineffects ${CMAKE_CURRENT_SOURCE_DIR}/effects ${CMAKE_CURRENT_SOURCE_DIR}/tabbox ${CMAKE_CURRENT_SOURCE_DIR}/platformsupport ) add_subdirectory(libkwineffects) if (KWIN_BUILD_KCMS) add_subdirectory(kcmkwin) endif() add_subdirectory(data) add_subdirectory(effects) add_subdirectory(scripts) add_subdirectory(tabbox) add_subdirectory(scripting) add_subdirectory(helpers) ########### next target ############### set(kwin_SRCS abstract_client.cpp abstract_opengl_context_attribute_builder.cpp abstract_output.cpp abstract_wayland_output.cpp activation.cpp appmenu.cpp atoms.cpp client_machine.cpp colorcorrection/clockskewnotifier.cpp colorcorrection/clockskewnotifierengine.cpp colorcorrection/colorcorrectdbusinterface.cpp colorcorrection/manager.cpp colorcorrection/suncalc.cpp composite.cpp cursor.cpp dbusinterface.cpp debug_console.cpp decorations/decoratedclient.cpp decorations/decorationbridge.cpp decorations/decorationpalette.cpp decorations/decorationrenderer.cpp decorations/decorations_logging.cpp decorations/settings.cpp deleted.cpp effectloader.cpp effects.cpp egl_context_attribute_builder.cpp events.cpp focuschain.cpp geometrytip.cpp gestures.cpp globalshortcuts.cpp group.cpp idle_inhibition.cpp input.cpp input_event.cpp input_event_spy.cpp internal_client.cpp keyboard_input.cpp keyboard_layout.cpp keyboard_layout_switching.cpp keyboard_repeat.cpp killwindow.cpp layers.cpp libinput/connection.cpp libinput/context.cpp libinput/device.cpp libinput/events.cpp libinput/libinput_logging.cpp linux_dmabuf.cpp logind.cpp main.cpp modifier_only_shortcuts.cpp moving_client_x11_filter.cpp netinfo.cpp onscreennotification.cpp options.cpp osd.cpp outline.cpp outputscreens.cpp overlaywindow.cpp placement.cpp platform.cpp pointer_input.cpp popup_input_filter.cpp rootinfo_filter.cpp rules.cpp rulebooksettings.cpp scene.cpp screenedge.cpp screenlockerwatcher.cpp screens.cpp scripting/dbuscall.cpp scripting/meta.cpp scripting/screenedgeitem.cpp scripting/scriptedeffect.cpp scripting/scripting.cpp scripting/scripting_logging.cpp scripting/scripting_model.cpp scripting/scriptingutils.cpp scripting/timer.cpp scripting/workspace_wrapper.cpp shadow.cpp sm.cpp subsurfacemonitor.cpp syncalarmx11filter.cpp + tablet_input.cpp thumbnailitem.cpp toplevel.cpp touch_hide_cursor_spy.cpp - tablet_input.cpp touch_input.cpp udev.cpp unmanaged.cpp useractions.cpp utils.cpp virtualdesktops.cpp virtualdesktopsdbustypes.cpp virtualkeyboard.cpp virtualkeyboard_dbus.cpp was_user_interaction_x11_filter.cpp wayland_cursor_theme.cpp wayland_server.cpp + waylandclient.cpp window_property_notify_x11_filter.cpp workspace.cpp x11client.cpp x11eventfilter.cpp xcbutils.cpp xdgshellclient.cpp xkb.cpp xwaylandclient.cpp xwl/xwayland_interface.cpp ) if (CMAKE_SYSTEM_NAME MATCHES "Linux") set(kwin_SRCS ${kwin_SRCS} colorcorrection/clockskewnotifierengine_linux.cpp ) endif() include(ECMQtDeclareLoggingCategory) ecm_qt_declare_logging_category(kwin_SRCS HEADER colorcorrect_logging.h IDENTIFIER KWIN_COLORCORRECTION CATEGORY_NAME kwin_colorcorrection DEFAULT_SEVERITY Critical ) if (KWIN_BUILD_TABBOX) include_directories(${Qt5Gui_PRIVATE_INCLUDE_DIRS}) set(kwin_SRCS ${kwin_SRCS} tabbox/clientmodel.cpp tabbox/desktopchain.cpp tabbox/desktopmodel.cpp tabbox/switcheritem.cpp tabbox/tabbox.cpp tabbox/tabbox_logging.cpp tabbox/tabboxconfig.cpp tabbox/tabboxhandler.cpp tabbox/x11_filter.cpp ) endif() if (KWIN_BUILD_ACTIVITIES) set(kwin_SRCS ${kwin_SRCS} activities.cpp ) endif() if (HAVE_LINUX_VT_H) set(kwin_SRCS ${kwin_SRCS} virtual_terminal.cpp ) endif() kconfig_add_kcfg_files(kwin_SRCS settings.kcfgc) kconfig_add_kcfg_files(kwin_SRCS colorcorrection/colorcorrect_settings.kcfgc) kconfig_add_kcfg_files(kwin_SRCS rulesettings.kcfgc) kconfig_add_kcfg_files(kwin_SRCS rulebooksettingsbase.kcfgc) qt5_add_dbus_adaptor(kwin_SRCS org.kde.KWin.xml dbusinterface.h KWin::DBusInterface) qt5_add_dbus_adaptor(kwin_SRCS org.kde.kwin.Compositing.xml dbusinterface.h KWin::CompositorDBusInterface) qt5_add_dbus_adaptor(kwin_SRCS org.kde.kwin.ColorCorrect.xml colorcorrection/colorcorrectdbusinterface.h KWin::ColorCorrect::ColorCorrectDBusInterface) qt5_add_dbus_adaptor(kwin_SRCS ${kwin_effects_dbus_xml} effects.h KWin::EffectsHandlerImpl) qt5_add_dbus_adaptor(kwin_SRCS org.kde.KWin.VirtualDesktopManager.xml dbusinterface.h KWin::VirtualDesktopManagerDBusInterface) qt5_add_dbus_adaptor(kwin_SRCS org.kde.KWin.Session.xml sm.h KWin::SessionManager) qt5_add_dbus_interface(kwin_SRCS ${KSCREENLOCKER_DBUS_INTERFACES_DIR}/kf5_org.freedesktop.ScreenSaver.xml screenlocker_interface) qt5_add_dbus_interface(kwin_SRCS ${KSCREENLOCKER_DBUS_INTERFACES_DIR}/org.kde.screensaver.xml kscreenlocker_interface) qt5_add_dbus_interface(kwin_SRCS org.kde.kappmenu.xml appmenu_interface) ki18n_wrap_ui(kwin_SRCS debug_console.ui shortcutdialog.ui ) ########### target link libraries ############### set(kwin_OWN_LIBS kwineffects kwin4_effect_builtins ) set(kwin_QT_LIBS Qt5::Concurrent Qt5::DBus Qt5::Quick Qt5::Script Qt5::Sensors ) set(kwin_KDE_LIBS KF5::ConfigCore KF5::ConfigWidgets KF5::CoreAddons KF5::GlobalAccel KF5::GlobalAccelPrivate KF5::I18n KF5::Notifications KF5::Package KF5::Plasma KF5::QuickAddons KF5::WindowSystem KDecoration2::KDecoration KDecoration2::KDecoration2Private PW::KScreenLocker ) set(kwin_XLIB_LIBS ${X11_ICE_LIB} ${X11_SM_LIB} ${X11_X11_LIB} ) set(kwin_XCB_LIBS XCB::COMPOSITE XCB::DAMAGE XCB::GLX XCB::ICCCM XCB::KEYSYMS XCB::RANDR XCB::RENDER XCB::SHAPE XCB::SHM XCB::SYNC XCB::XCB XCB::XFIXES ) set(kwin_WAYLAND_LIBS KF5::WaylandClient Plasma::KWaylandServer Wayland::Cursor XKB::XKB ${CMAKE_THREAD_LIBS_INIT} ) if (KWIN_BUILD_ACTIVITIES) set(kwin_KDE_LIBS ${kwin_KDE_LIBS} KF5::Activities) endif() set(kwinLibs ${kwin_OWN_LIBS} ${kwin_QT_LIBS} ${kwin_KDE_LIBS} ${kwin_XLIB_LIBS} ${kwin_XCB_LIBS} ${kwin_WAYLAND_LIBS} ${UDEV_LIBS} Libinput::Libinput ) add_library(kwin SHARED ${kwin_SRCS}) if (Libinput_VERSION_STRING VERSION_GREATER 1.14) target_compile_definitions(kwin PRIVATE -DLIBINPUT_HAS_TOTEM) endif () set_target_properties(kwin PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} ) target_link_libraries(kwin ${kwinLibs}) generate_export_header(kwin EXPORT_FILE_NAME kwin_export.h) target_link_libraries(kwin kwinglutils ${epoxy_LIBRARY}) add_executable(kwin_x11 main_x11.cpp) target_link_libraries(kwin_x11 kwin KF5::Crash Qt5::X11Extras) install(TARGETS kwin ${INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) install(TARGETS kwin_x11 ${INSTALL_TARGETS_DEFAULT_ARGS}) set(kwin_XWAYLAND_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/xwl/clipboard.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xwl/databridge.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xwl/dnd.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xwl/drag.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xwl/drag_wl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xwl/drag_x.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xwl/selection.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xwl/selection_source.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xwl/transfer.cpp ${CMAKE_CURRENT_SOURCE_DIR}/xwl/xwayland.cpp ) include(ECMQtDeclareLoggingCategory) ecm_qt_declare_logging_category(kwin_XWAYLAND_SRCS HEADER xwayland_logging.h IDENTIFIER KWIN_XWL CATEGORY_NAME kwin_xwl DEFAULT_SEVERITY Critical ) set(kwin_WAYLAND_SRCS main_wayland.cpp tabletmodemanager.cpp ) add_executable(kwin_wayland ${kwin_WAYLAND_SRCS} ${kwin_XWAYLAND_SRCS}) target_link_libraries(kwin_wayland kwin KF5::Crash) if (HAVE_LIBCAP) target_link_libraries(kwin_wayland ${Libcap_LIBRARIES}) endif() install(TARGETS kwin_wayland ${INSTALL_TARGETS_DEFAULT_ARGS}) if (HAVE_LIBCAP) install( CODE "execute_process( COMMAND ${SETCAP_EXECUTABLE} CAP_SYS_NICE=+ep \$ENV{DESTDIR}${CMAKE_INSTALL_FULL_BINDIR}/kwin_wayland)" ) endif() add_subdirectory(platformsupport) add_subdirectory(plugins) ########### install files ############### install(FILES kwin.kcfg DESTINATION ${KCFG_INSTALL_DIR} RENAME ${KWIN_NAME}.kcfg) install(FILES colorcorrection/colorcorrect_settings.kcfg DESTINATION ${KCFG_INSTALL_DIR} RENAME ${KWIN_NAME}_colorcorrect.kcfg) install(FILES kwin.notifyrc DESTINATION ${KNOTIFYRC_INSTALL_DIR} RENAME ${KWIN_NAME}.notifyrc) install( FILES org.kde.KWin.VirtualDesktopManager.xml org.kde.KWin.xml org.kde.kwin.ColorCorrect.xml org.kde.kwin.Compositing.xml org.kde.kwin.Effects.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/kwin_export.h DESTINATION ${INCLUDE_INSTALL_DIR} COMPONENT Devel) # Install the KWin/Script service type install(FILES scripting/kwinscript.desktop DESTINATION ${SERVICETYPES_INSTALL_DIR}) add_subdirectory(qml) if (BUILD_TESTING) add_subdirectory(autotests) add_subdirectory(tests) endif() if (KF5DocTools_FOUND) add_subdirectory(doc) endif() add_subdirectory(kconf_update) # add clang-format target for all our real source files file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h) kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES}) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) include(CMakePackageConfigHelpers) set(CMAKECONFIG_INSTALL_DIR "${CMAKECONFIG_INSTALL_PREFIX}/KWinDBusInterface") configure_package_config_file(KWinDBusInterfaceConfig.cmake.in "${CMAKE_CURRENT_BINARY_DIR}/KWinDBusInterfaceConfig.cmake" PATH_VARS KDE_INSTALL_DBUSINTERFACEDIR INSTALL_DESTINATION ${CMAKECONFIG_INSTALL_DIR}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/KWinDBusInterfaceConfig.cmake DESTINATION ${CMAKECONFIG_INSTALL_DIR}) diff --git a/abstract_client.cpp b/abstract_client.cpp index e3a3c8438..ef810406a 100644 --- a/abstract_client.cpp +++ b/abstract_client.cpp @@ -1,3409 +1,3415 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 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 "useractions.h" #include "workspace.h" #include "wayland_server.h" #include #include #include #include #include #include namespace KWin { static inline int sign(int v) { return (v > 0) - (v < 0); } QHash> AbstractClient::s_palettes; std::shared_ptr AbstractClient::s_defaultPalette; AbstractClient::AbstractClient() : Toplevel() #ifdef KWIN_BUILD_TABBOX , m_tabBoxClient(QSharedPointer(new TabBox::TabBoxClientImpl(this))) #endif , m_colorScheme(QStringLiteral("kdeglobals")) { connect(this, &AbstractClient::clientStartUserMovedResized, this, &AbstractClient::moveResizedChanged); connect(this, &AbstractClient::clientFinishUserMovedResized, this, &AbstractClient::moveResizedChanged); connect(this, &AbstractClient::clientStartUserMovedResized, this, &AbstractClient::removeCheckScreenConnection); connect(this, &AbstractClient::clientFinishUserMovedResized, this, &AbstractClient::setupCheckScreenConnection); connect(this, &AbstractClient::paletteChanged, this, &AbstractClient::triggerDecorationRepaint); connect(Decoration::DecorationBridge::self(), &QObject::destroyed, this, &AbstractClient::destroyDecoration); // If the user manually moved the window, don't restore it after the keyboard closes connect(this, &AbstractClient::clientFinishUserMovedResized, this, [this] () { m_keyboardGeometryRestore = QRect(); }); connect(this, qOverload(&AbstractClient::clientMaximizedStateChanged), this, [this] () { m_keyboardGeometryRestore = QRect(); }); connect(this, &AbstractClient::fullScreenChanged, this, [this] () { m_keyboardGeometryRestore = QRect(); }); // replace on-screen-display on size changes connect(this, &AbstractClient::frameGeometryChanged, this, [this] (Toplevel *c, const QRect &old) { Q_UNUSED(c) if (isOnScreenDisplay() && !frameGeometry().isEmpty() && old.size() != frameGeometry().size() && !isInitialPositionSet()) { GeometryUpdatesBlocker blocker(this); const QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop()); Placement::self()->place(this, area); setGeometryRestore(frameGeometry()); } } ); connect(this, &AbstractClient::paddingChanged, this, [this]() { m_visibleRectBeforeGeometryUpdate = visibleRect(); }); connect(ApplicationMenu::self(), &ApplicationMenu::applicationMenuEnabledChanged, this, [this] { emit hasApplicationMenuChanged(hasApplicationMenu()); }); } AbstractClient::~AbstractClient() { Q_ASSERT(m_blockGeometryUpdates == 0); Q_ASSERT(m_decoration.decoration == nullptr); } void AbstractClient::updateMouseGrab() { } bool AbstractClient::belongToSameApplication(const AbstractClient *c1, const AbstractClient *c2, SameApplicationChecks checks) { return c1->belongsToSameApplication(c2, checks); } bool AbstractClient::isTransient() const { return false; } void AbstractClient::setClientShown(bool shown) { Q_UNUSED(shown) } xcb_timestamp_t AbstractClient::userTime() const { return XCB_TIME_CURRENT_TIME; } void AbstractClient::setSkipSwitcher(bool set) { set = rules()->checkSkipSwitcher(set); if (set == skipSwitcher()) return; m_skipSwitcher = set; doSetSkipSwitcher(); updateWindowRules(Rules::SkipSwitcher); emit skipSwitcherChanged(); } void AbstractClient::setSkipPager(bool b) { b = rules()->checkSkipPager(b); if (b == skipPager()) return; m_skipPager = b; doSetSkipPager(); updateWindowRules(Rules::SkipPager); emit skipPagerChanged(); } void AbstractClient::doSetSkipPager() { } void AbstractClient::setSkipTaskbar(bool b) { int was_wants_tab_focus = wantsTabFocus(); if (b == skipTaskbar()) return; m_skipTaskbar = b; doSetSkipTaskbar(); updateWindowRules(Rules::SkipTaskbar); if (was_wants_tab_focus != wantsTabFocus()) { FocusChain::self()->update(this, isActive() ? FocusChain::MakeFirst : FocusChain::Update); } emit skipTaskbarChanged(); } void AbstractClient::setOriginalSkipTaskbar(bool b) { m_originalSkipTaskbar = rules()->checkSkipTaskbar(b); setSkipTaskbar(m_originalSkipTaskbar); } void AbstractClient::doSetSkipTaskbar() { } void AbstractClient::doSetSkipSwitcher() { } void AbstractClient::setIcon(const QIcon &icon) { m_icon = icon; emit iconChanged(); } void AbstractClient::setActive(bool act) { if (m_active == act) { return; } m_active = act; const int ruledOpacity = m_active ? rules()->checkOpacityActive(qRound(opacity() * 100.0)) : rules()->checkOpacityInactive(qRound(opacity() * 100.0)); setOpacity(ruledOpacity / 100.0); workspace()->setActiveClient(act ? this : nullptr); if (!m_active) cancelAutoRaise(); if (!m_active && shadeMode() == ShadeActivated) setShade(ShadeNormal); StackingUpdatesBlocker blocker(workspace()); workspace()->updateClientLayer(this); // active windows may get different layer auto mainclients = mainClients(); for (auto it = mainclients.constBegin(); it != mainclients.constEnd(); ++it) if ((*it)->isFullScreen()) // fullscreens go high even if their transient is active workspace()->updateClientLayer(*it); doSetActive(); emit activeChanged(); updateMouseGrab(); } void AbstractClient::doSetActive() { } Layer AbstractClient::layer() const { if (m_layer == UnknownLayer) const_cast< AbstractClient* >(this)->m_layer = belongsToLayer(); return m_layer; } void AbstractClient::updateLayer() { if (layer() == belongsToLayer()) return; StackingUpdatesBlocker blocker(workspace()); invalidateLayer(); // invalidate, will be updated when doing restacking for (auto it = transients().constBegin(), end = transients().constEnd(); it != end; ++it) (*it)->updateLayer(); } void AbstractClient::placeIn(const QRect &area) { // TODO: Get rid of this method eventually. We need to call setGeometryRestore() because // checkWorkspacePosition() operates on geometryRestore() and because of quick tiling. Placement::self()->place(this, area); setGeometryRestore(frameGeometry()); } void AbstractClient::invalidateLayer() { m_layer = UnknownLayer; } Layer AbstractClient::belongsToLayer() const { // NOTICE while showingDesktop, desktops move to the AboveLayer // (interchangeable w/ eg. yakuake etc. which will at first remain visible) // and the docks move into the NotificationLayer (which is between Above- and // ActiveLayer, so that active fullscreen windows will still cover everything) // Since the desktop is also activated, nothing should be in the ActiveLayer, though if (isInternal()) return UnmanagedLayer; if (isLockScreen()) return UnmanagedLayer; if (isDesktop()) return workspace()->showingDesktop() ? AboveLayer : DesktopLayer; if (isSplash()) // no damn annoying splashscreens return NormalLayer; // getting in the way of everything else if (isDock()) { if (workspace()->showingDesktop()) return NotificationLayer; return layerForDock(); } if (isOnScreenDisplay()) return OnScreenDisplayLayer; if (isNotification()) return NotificationLayer; if (isCriticalNotification()) return CriticalNotificationLayer; if (workspace()->showingDesktop() && belongsToDesktop()) { return AboveLayer; } if (keepBelow()) return BelowLayer; if (isActiveFullScreen()) return ActiveLayer; if (keepAbove()) return AboveLayer; return NormalLayer; } bool AbstractClient::belongsToDesktop() const { return false; } Layer AbstractClient::layerForDock() const { // slight hack for the 'allow window to cover panel' Kicker setting // don't move keepbelow docks below normal window, but only to the same // layer, so that both may be raised to cover the other if (keepBelow()) return NormalLayer; if (keepAbove()) // slight hack for the autohiding panels return AboveLayer; return DockLayer; } void AbstractClient::setKeepAbove(bool b) { b = rules()->checkKeepAbove(b); if (b && !rules()->checkKeepBelow(false)) setKeepBelow(false); if (b == keepAbove()) { return; } m_keepAbove = b; doSetKeepAbove(); workspace()->updateClientLayer(this); updateWindowRules(Rules::Above); emit keepAboveChanged(m_keepAbove); } void AbstractClient::doSetKeepAbove() { } void AbstractClient::setKeepBelow(bool b) { b = rules()->checkKeepBelow(b); if (b && !rules()->checkKeepAbove(false)) setKeepAbove(false); if (b == keepBelow()) { return; } m_keepBelow = b; doSetKeepBelow(); workspace()->updateClientLayer(this); updateWindowRules(Rules::Below); emit keepBelowChanged(m_keepBelow); } void AbstractClient::doSetKeepBelow() { } void AbstractClient::startAutoRaise() { delete m_autoRaiseTimer; m_autoRaiseTimer = new QTimer(this); connect(m_autoRaiseTimer, &QTimer::timeout, this, &AbstractClient::autoRaise); m_autoRaiseTimer->setSingleShot(true); m_autoRaiseTimer->start(options->autoRaiseInterval()); } void AbstractClient::cancelAutoRaise() { delete m_autoRaiseTimer; m_autoRaiseTimer = nullptr; } void AbstractClient::autoRaise() { workspace()->raiseClient(this); cancelAutoRaise(); } bool AbstractClient::wantsTabFocus() const { return (isNormalWindow() || isDialog()) && wantsInput(); } bool AbstractClient::isSpecialWindow() const { // TODO return isDesktop() || isDock() || isSplash() || isToolbar() || isNotification() || isOnScreenDisplay() || isCriticalNotification(); } void AbstractClient::demandAttention(bool set) { if (isActive()) set = false; if (m_demandsAttention == set) return; m_demandsAttention = set; doSetDemandsAttention(); workspace()->clientAttentionChanged(this, set); emit demandsAttentionChanged(); } void AbstractClient::doSetDemandsAttention() { } void AbstractClient::setDesktop(int desktop) { const int numberOfDesktops = VirtualDesktopManager::self()->count(); if (desktop != NET::OnAllDesktops) // Do range check desktop = qMax(1, qMin(numberOfDesktops, desktop)); desktop = qMin(numberOfDesktops, rules()->checkDesktop(desktop)); QVector desktops; if (desktop != NET::OnAllDesktops) { desktops << VirtualDesktopManager::self()->desktopForX11Id(desktop); } setDesktops(desktops); } void AbstractClient::setDesktops(QVector desktops) { //on x11 we can have only one desktop at a time if (kwinApp()->operationMode() == Application::OperationModeX11 && desktops.size() > 1) { desktops = QVector({desktops.last()}); } if (desktops == m_desktops) { return; } int was_desk = AbstractClient::desktop(); const bool wasOnCurrentDesktop = isOnCurrentDesktop() && was_desk >= 0; m_desktops = desktops; if (windowManagementInterface()) { if (m_desktops.isEmpty()) { windowManagementInterface()->setOnAllDesktops(true); } else { windowManagementInterface()->setOnAllDesktops(false); auto currentDesktops = windowManagementInterface()->plasmaVirtualDesktops(); for (auto desktop: m_desktops) { if (!currentDesktops.contains(desktop->id())) { windowManagementInterface()->addPlasmaVirtualDesktop(desktop->id()); } else { currentDesktops.removeOne(desktop->id()); } } for (auto desktopId: currentDesktops) { windowManagementInterface()->removePlasmaVirtualDesktop(desktopId); } } } if (info) { info->setDesktop(desktop()); } if ((was_desk == NET::OnAllDesktops) != (desktop() == NET::OnAllDesktops)) { // onAllDesktops changed workspace()->updateOnAllDesktopsOfTransients(this); } auto transients_stacking_order = workspace()->ensureStackingOrder(transients()); for (auto it = transients_stacking_order.constBegin(); it != transients_stacking_order.constEnd(); ++it) (*it)->setDesktops(desktops); if (isModal()) // if a modal dialog is moved, move the mainwindow with it as otherwise // the (just moved) modal dialog will confusingly return to the mainwindow with // the next desktop change { foreach (AbstractClient * c2, mainClients()) c2->setDesktops(desktops); } doSetDesktop(); FocusChain::self()->update(this, FocusChain::MakeFirst); updateWindowRules(Rules::Desktop); emit desktopChanged(); if (wasOnCurrentDesktop != isOnCurrentDesktop()) emit desktopPresenceChanged(this, was_desk); emit x11DesktopIdsChanged(); } void AbstractClient::doSetDesktop() { } void AbstractClient::enterDesktop(VirtualDesktop *virtualDesktop) { if (m_desktops.contains(virtualDesktop)) { return; } auto desktops = m_desktops; desktops.append(virtualDesktop); setDesktops(desktops); } void AbstractClient::leaveDesktop(VirtualDesktop *virtualDesktop) { QVector currentDesktops; if (m_desktops.isEmpty()) { currentDesktops = VirtualDesktopManager::self()->desktops(); } else { currentDesktops = m_desktops; } if (!currentDesktops.contains(virtualDesktop)) { return; } auto desktops = currentDesktops; desktops.removeOne(virtualDesktop); setDesktops(desktops); } void AbstractClient::setOnAllDesktops(bool b) { if ((b && isOnAllDesktops()) || (!b && !isOnAllDesktops())) return; if (b) setDesktop(NET::OnAllDesktops); else setDesktop(VirtualDesktopManager::self()->current()); } QVector AbstractClient::x11DesktopIds() const { const auto desks = desktops(); QVector x11Ids; x11Ids.reserve(desks.count()); std::transform(desks.constBegin(), desks.constEnd(), std::back_inserter(x11Ids), [] (const VirtualDesktop *vd) { return vd->x11DesktopNumber(); } ); return x11Ids; } ShadeMode AbstractClient::shadeMode() const { return m_shadeMode; } bool AbstractClient::isShadeable() const { return false; } void AbstractClient::setShade(bool set) { set ? setShade(ShadeNormal) : setShade(ShadeNone); } void AbstractClient::setShade(ShadeMode mode) { if (!isShadeable()) return; if (mode == ShadeHover && isMove()) return; // causes geometry breaks and is probably nasty if (isSpecialWindow() || noBorder()) mode = ShadeNone; mode = rules()->checkShade(mode); if (m_shadeMode == mode) return; const bool wasShade = isShade(); const ShadeMode previousShadeMode = shadeMode(); m_shadeMode = mode; if (wasShade == isShade()) { // Decoration may want to update after e.g. hover-shade changes emit shadeChanged(); return; // No real change in shaded state } Q_ASSERT(isDecorated()); GeometryUpdatesBlocker blocker(this); doSetShade(previousShadeMode); discardWindowPixmap(); updateWindowRules(Rules::Shade); emit shadeChanged(); } void AbstractClient::doSetShade(ShadeMode previousShadeMode) { Q_UNUSED(previousShadeMode) } void AbstractClient::shadeHover() { setShade(ShadeHover); cancelShadeHoverTimer(); } void AbstractClient::shadeUnhover() { setShade(ShadeNormal); cancelShadeHoverTimer(); } void AbstractClient::startShadeHoverTimer() { if (!isShade()) return; m_shadeHoverTimer = new QTimer(this); connect(m_shadeHoverTimer, &QTimer::timeout, this, &AbstractClient::shadeHover); m_shadeHoverTimer->setSingleShot(true); m_shadeHoverTimer->start(options->shadeHoverInterval()); } void AbstractClient::startShadeUnhoverTimer() { if (m_shadeMode == ShadeHover && !isMoveResize() && !isMoveResizePointerButtonDown()) { m_shadeHoverTimer = new QTimer(this); connect(m_shadeHoverTimer, &QTimer::timeout, this, &AbstractClient::shadeUnhover); m_shadeHoverTimer->setSingleShot(true); m_shadeHoverTimer->start(options->shadeHoverInterval()); } } void AbstractClient::cancelShadeHoverTimer() { delete m_shadeHoverTimer; m_shadeHoverTimer = nullptr; } void AbstractClient::toggleShade() { // If the mode is ShadeHover or ShadeActive, cancel shade too. setShade(shadeMode() == ShadeNone ? ShadeNormal : ShadeNone); } AbstractClient::Position AbstractClient::titlebarPosition() const { // TODO: still needed, remove? return PositionTop; } bool AbstractClient::titlebarPositionUnderMouse() const { if (!isDecorated()) { return false; } const auto sectionUnderMouse = decoration()->sectionUnderMouse(); if (sectionUnderMouse == Qt::TitleBarArea) { return true; } // check other sections based on titlebarPosition switch (titlebarPosition()) { case AbstractClient::PositionTop: return (sectionUnderMouse == Qt::TopLeftSection || sectionUnderMouse == Qt::TopSection || sectionUnderMouse == Qt::TopRightSection); case AbstractClient::PositionLeft: return (sectionUnderMouse == Qt::TopLeftSection || sectionUnderMouse == Qt::LeftSection || sectionUnderMouse == Qt::BottomLeftSection); case AbstractClient::PositionRight: return (sectionUnderMouse == Qt::BottomRightSection || sectionUnderMouse == Qt::RightSection || sectionUnderMouse == Qt::TopRightSection); case AbstractClient::PositionBottom: return (sectionUnderMouse == Qt::BottomLeftSection || sectionUnderMouse == Qt::BottomSection || sectionUnderMouse == Qt::BottomRightSection); default: // nothing return false; } } void AbstractClient::setMinimized(bool set) { set ? minimize() : unminimize(); } void AbstractClient::minimize(bool avoid_animation) { if (!isMinimizable() || isMinimized()) return; m_minimized = true; doMinimize(); updateWindowRules(Rules::Minimize); FocusChain::self()->update(this, FocusChain::MakeFirstMinimized); // TODO: merge signal with s_minimized addWorkspaceRepaint(visibleRect()); emit clientMinimized(this, !avoid_animation); emit minimizedChanged(); } void AbstractClient::unminimize(bool avoid_animation) { if (!isMinimized()) return; if (rules()->checkMinimize(false)) { return; } m_minimized = false; doMinimize(); updateWindowRules(Rules::Minimize); emit clientUnminimized(this, !avoid_animation); emit minimizedChanged(); } void AbstractClient::doMinimize() { } QPalette AbstractClient::palette() const { if (!m_palette) { return QPalette(); } return m_palette->palette(); } const Decoration::DecorationPalette *AbstractClient::decorationPalette() const { return m_palette.get(); } 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()); emit colorSchemeChanged(); } } void AbstractClient::handlePaletteChange() { emit paletteChanged(palette()); } void AbstractClient::keepInArea(QRect area, bool partial) { if (partial) { // increase the area so that can have only 100 pixels in the area area.setLeft(qMin(area.left() - width() + 100, area.left())); area.setTop(qMin(area.top() - height() + 100, area.top())); area.setRight(qMax(area.right() + width() - 100, area.right())); area.setBottom(qMax(area.bottom() + height() - 100, area.bottom())); } if (!partial) { // resize to fit into area if (area.width() < width() || area.height() < height()) resizeWithChecks(size().boundedTo(area.size())); } int tx = x(), ty = y(); if (frameGeometry().right() > area.right() && width() <= area.width()) tx = area.right() - width() + 1; if (frameGeometry().bottom() > area.bottom() && height() <= area.height()) ty = area.bottom() - height() + 1; if (!area.contains(frameGeometry().topLeft())) { if (tx < area.x()) tx = area.x(); if (ty < area.y()) ty = area.y(); } if (tx != x() || ty != y()) move(tx, ty); } /** * Returns the maximum client size, not the maximum frame size. */ QSize AbstractClient::maxSize() const { return rules()->checkMaxSize(QSize(INT_MAX, INT_MAX)); } /** * Returns the minimum client size, not the minimum frame size. */ QSize AbstractClient::minSize() const { return rules()->checkMinSize(QSize(0, 0)); } void AbstractClient::blockGeometryUpdates(bool block) { if (block) { if (m_blockGeometryUpdates == 0) m_pendingGeometryUpdate = PendingGeometryNone; ++m_blockGeometryUpdates; } else { if (--m_blockGeometryUpdates == 0) { if (m_pendingGeometryUpdate != PendingGeometryNone) { if (isShade()) setFrameGeometry(QRect(pos(), adjustedSize()), NormalGeometrySet); else setFrameGeometry(frameGeometry(), NormalGeometrySet); m_pendingGeometryUpdate = PendingGeometryNone; } } } } void AbstractClient::maximize(MaximizeMode m) { setMaximize(m & MaximizeVertical, m & MaximizeHorizontal); } void AbstractClient::setMaximize(bool vertically, bool horizontally) { // changeMaximize() flips the state, so change from set->flip const MaximizeMode oldMode = maximizeMode(); changeMaximize( oldMode & MaximizeHorizontal ? !horizontally : horizontally, oldMode & MaximizeVertical ? !vertically : vertically, false); const MaximizeMode newMode = maximizeMode(); if (oldMode != newMode) { emit clientMaximizedStateChanged(this, newMode); emit clientMaximizedStateChanged(this, vertically, horizontally); } } void AbstractClient::move(int x, int y, ForceGeometry_t force) { // resuming geometry updates is handled only in setGeometry() Q_ASSERT(pendingGeometryUpdate() == PendingGeometryNone || areGeometryUpdatesBlocked()); QPoint p(x, y); if (!areGeometryUpdatesBlocked() && p != rules()->checkPosition(p)) { qCDebug(KWIN_CORE) << "forced position fail:" << p << ":" << rules()->checkPosition(p); } if (force == NormalGeometrySet && m_frameGeometry.topLeft() == p) return; m_frameGeometry.moveTopLeft(p); if (areGeometryUpdatesBlocked()) { if (pendingGeometryUpdate() == PendingGeometryForced) {} // maximum, nothing needed else if (force == ForceGeometrySet) setPendingGeometryUpdate(PendingGeometryForced); else setPendingGeometryUpdate(PendingGeometryNormal); return; } doMove(x, y); updateWindowRules(Rules::Position); screens()->setCurrent(this); workspace()->updateStackingOrder(); // client itself is not damaged emit frameGeometryChanged(this, frameGeometryBeforeUpdateBlocking()); addRepaintDuringGeometryUpdates(); updateGeometryBeforeUpdateBlocking(); } bool AbstractClient::startMoveResize() { Q_ASSERT(!isMoveResize()); Q_ASSERT(QWidget::keyboardGrabber() == nullptr); Q_ASSERT(QWidget::mouseGrabber() == nullptr); stopDelayedMoveResize(); if (QApplication::activePopupWidget() != nullptr) return false; // popups have grab if (isFullScreen() && (screens()->count() < 2 || !isMovableAcrossScreens())) return false; if (!doStartMoveResize()) { return false; } invalidateDecorationDoubleClickTimer(); setMoveResize(true); workspace()->setMoveResizeClient(this); const Position mode = moveResizePointerMode(); if (mode != PositionCenter) { // means "isResize()" but moveResizeMode = true is set below if (maximizeMode() == MaximizeFull) { // partial is cond. reset in finishMoveResize setGeometryRestore(frameGeometry()); // "restore" to current geometry setMaximize(false, false); } } if (quickTileMode() != QuickTileMode(QuickTileFlag::None) && mode != PositionCenter) { // Cannot use isResize() yet // Exit quick tile mode when the user attempts to resize a tiled window updateQuickTileMode(QuickTileFlag::None); // Do so without restoring original geometry setGeometryRestore(frameGeometry()); emit quickTileModeChanged(); } updateHaveResizeEffect(); updateInitialMoveResizeGeometry(); checkUnrestrictedMoveResize(); emit clientStartUserMovedResized(this); if (ScreenEdges::self()->isDesktopSwitchingMovingClients()) ScreenEdges::self()->reserveDesktopSwitching(true, Qt::Vertical|Qt::Horizontal); return true; } void AbstractClient::finishMoveResize(bool cancel) { GeometryUpdatesBlocker blocker(this); const bool wasResize = isResize(); // store across leaveMoveResize leaveMoveResize(); + doFinishMoveResize(); + if (cancel) setFrameGeometry(initialMoveResizeGeometry()); else { const QRect &moveResizeGeom = moveResizeGeometry(); if (wasResize) { const bool restoreH = maximizeMode() == MaximizeHorizontal && moveResizeGeom.width() != initialMoveResizeGeometry().width(); const bool restoreV = maximizeMode() == MaximizeVertical && moveResizeGeom.height() != initialMoveResizeGeometry().height(); if (restoreH || restoreV) { changeMaximize(restoreH, restoreV, false); } } setFrameGeometry(moveResizeGeom); } checkScreen(); // needs to be done because clientFinishUserMovedResized has not yet re-activated online alignment if (screen() != moveResizeStartScreen()) { workspace()->sendClientToScreen(this, screen()); // checks rule validity if (maximizeMode() != MaximizeRestore) checkWorkspacePosition(); } if (isElectricBorderMaximizing()) { setQuickTileMode(electricBorderMode()); setElectricBorderMaximizing(false); } else if (!cancel) { QRect geom_restore = geometryRestore(); if (!(maximizeMode() & MaximizeHorizontal)) { geom_restore.setX(frameGeometry().x()); geom_restore.setWidth(frameGeometry().width()); } if (!(maximizeMode() & MaximizeVertical)) { geom_restore.setY(frameGeometry().y()); geom_restore.setHeight(frameGeometry().height()); } setGeometryRestore(geom_restore); } // FRAME update(); emit clientFinishUserMovedResized(this); } // This function checks if it actually makes sense to perform a restricted move/resize. // If e.g. the titlebar is already outside of the workarea, there's no point in performing // a restricted move resize, because then e.g. resize would also move the window (#74555). // NOTE: Most of it is duplicated from handleMoveResize(). void AbstractClient::checkUnrestrictedMoveResize() { if (isUnrestrictedMoveResize()) return; const QRect &moveResizeGeom = moveResizeGeometry(); QRect desktopArea = workspace()->clientArea(WorkArea, moveResizeGeom.center(), desktop()); int left_marge, right_marge, top_marge, bottom_marge, titlebar_marge; // restricted move/resize - keep at least part of the titlebar always visible // how much must remain visible when moved away in that direction left_marge = qMin(100 + borderRight(), moveResizeGeom.width()); right_marge = qMin(100 + borderLeft(), moveResizeGeom.width()); // width/height change with opaque resizing, use the initial ones titlebar_marge = initialMoveResizeGeometry().height(); top_marge = borderBottom(); bottom_marge = borderTop(); if (isResize()) { if (moveResizeGeom.bottom() < desktopArea.top() + top_marge) setUnrestrictedMoveResize(true); if (moveResizeGeom.top() > desktopArea.bottom() - bottom_marge) setUnrestrictedMoveResize(true); if (moveResizeGeom.right() < desktopArea.left() + left_marge) setUnrestrictedMoveResize(true); if (moveResizeGeom.left() > desktopArea.right() - right_marge) setUnrestrictedMoveResize(true); if (!isUnrestrictedMoveResize() && moveResizeGeom.top() < desktopArea.top()) // titlebar mustn't go out setUnrestrictedMoveResize(true); } if (isMove()) { if (moveResizeGeom.bottom() < desktopArea.top() + titlebar_marge - 1) setUnrestrictedMoveResize(true); // no need to check top_marge, titlebar_marge already handles it if (moveResizeGeom.top() > desktopArea.bottom() - bottom_marge + 1) // titlebar mustn't go out setUnrestrictedMoveResize(true); if (moveResizeGeom.right() < desktopArea.left() + left_marge) setUnrestrictedMoveResize(true); if (moveResizeGeom.left() > desktopArea.right() - right_marge) setUnrestrictedMoveResize(true); } } // When the user pressed mouse on the titlebar, don't activate move 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]() { Q_ASSERT(isMoveResizePointerButtonDown()); if (!startMoveResize()) { setMoveResizePointerButtonDown(false); } updateCursor(); stopDelayedMoveResize(); } ); m_moveResize.delayedTimer->start(QApplication::startDragTime()); } void AbstractClient::stopDelayedMoveResize() { delete m_moveResize.delayedTimer; m_moveResize.delayedTimer = nullptr; } void AbstractClient::updateMoveResize(const QPointF ¤tGlobalCursor) { handleMoveResize(pos(), currentGlobalCursor.toPoint()); } void AbstractClient::handleMoveResize(const QPoint &local, const QPoint &global) { const QRect oldGeo = frameGeometry(); handleMoveResize(local.x(), local.y(), global.x(), global.y()); if (!isFullScreen() && isMove()) { if (quickTileMode() != QuickTileMode(QuickTileFlag::None) && oldGeo != frameGeometry()) { GeometryUpdatesBlocker blocker(this); setQuickTileMode(QuickTileFlag::None); const QRect &geom_restore = geometryRestore(); setMoveOffset(QPoint(double(moveOffset().x()) / double(oldGeo.width()) * double(geom_restore.width()), double(moveOffset().y()) / double(oldGeo.height()) * double(geom_restore.height()))); if (rules()->checkMaximize(MaximizeRestore) == MaximizeRestore) setMoveResizeGeometry(geom_restore); handleMoveResize(local.x(), local.y(), global.x(), global.y()); // fix position } else if (quickTileMode() == QuickTileMode(QuickTileFlag::None) && isResizable()) { checkQuickTilingMaximizationZones(global.x(), global.y()); } } } void AbstractClient::handleMoveResize(int x, int y, int x_root, int y_root) { if (isWaitingForMoveResizeSync()) return; // we're still waiting for the client or the timeout const Position mode = moveResizePointerMode(); if ((mode == PositionCenter && !isMovableAcrossScreens()) || (mode != PositionCenter && (isShade() || !isResizable()))) return; if (!isMoveResize()) { QPoint p(QPoint(x/* - padding_left*/, y/* - padding_top*/) - moveOffset()); if (p.manhattanLength() >= QApplication::startDragDistance()) { if (!startMoveResize()) { setMoveResizePointerButtonDown(false); updateCursor(); return; } updateCursor(); } else return; } // ShadeHover or ShadeActive, ShadeNormal was already avoided above if (mode != PositionCenter && shadeMode() != ShadeNone) setShade(ShadeNone); QPoint globalPos(x_root, y_root); // these two points limit the geometry rectangle, i.e. if bottomleft resizing is done, // the bottomleft corner should be at is at (topleft.x(), bottomright().y()) QPoint topleft = globalPos - moveOffset(); QPoint bottomright = globalPos + invertedMoveOffset(); QRect previousMoveResizeGeom = moveResizeGeometry(); // TODO move whole group when moving its leader or when the leader is not mapped? auto titleBarRect = [this](bool &transposed, int &requiredPixels) -> QRect { const QRect &moveResizeGeom = moveResizeGeometry(); QRect r(moveResizeGeom); r.moveTopLeft(QPoint(0,0)); switch (titlebarPosition()) { default: case PositionTop: r.setHeight(borderTop()); break; case PositionLeft: r.setWidth(borderLeft()); transposed = true; break; case PositionBottom: r.setTop(r.bottom() - borderBottom()); break; case PositionRight: r.setLeft(r.right() - borderRight()); transposed = true; break; } // When doing a restricted move we must always keep 100px of the titlebar // visible to allow the user to be able to move it again. requiredPixels = qMin(100 * (transposed ? r.width() : r.height()), moveResizeGeom.width() * moveResizeGeom.height()); return r; }; bool update = false; if (isResize()) { QRect orig = initialMoveResizeGeometry(); SizeMode sizeMode = SizeModeAny; auto calculateMoveResizeGeom = [this, &topleft, &bottomright, &orig, &sizeMode, &mode]() { switch(mode) { case PositionTopLeft: setMoveResizeGeometry(QRect(topleft, orig.bottomRight())); break; case PositionBottomRight: setMoveResizeGeometry(QRect(orig.topLeft(), bottomright)); break; case PositionBottomLeft: setMoveResizeGeometry(QRect(QPoint(topleft.x(), orig.y()), QPoint(orig.right(), bottomright.y()))); break; case PositionTopRight: setMoveResizeGeometry(QRect(QPoint(orig.x(), topleft.y()), QPoint(bottomright.x(), orig.bottom()))); break; case PositionTop: setMoveResizeGeometry(QRect(QPoint(orig.left(), topleft.y()), orig.bottomRight())); sizeMode = SizeModeFixedH; // try not to affect height break; case PositionBottom: setMoveResizeGeometry(QRect(orig.topLeft(), QPoint(orig.right(), bottomright.y()))); sizeMode = SizeModeFixedH; break; case PositionLeft: setMoveResizeGeometry(QRect(QPoint(topleft.x(), orig.top()), orig.bottomRight())); sizeMode = SizeModeFixedW; break; case PositionRight: setMoveResizeGeometry(QRect(orig.topLeft(), QPoint(bottomright.x(), orig.bottom()))); sizeMode = SizeModeFixedW; break; case PositionCenter: default: abort(); break; } }; // first resize (without checking constrains), then snap, then check bounds, then check constrains calculateMoveResizeGeom(); // adjust new size to snap to other windows/borders setMoveResizeGeometry(workspace()->adjustClientSize(this, moveResizeGeometry(), mode)); if (!isUnrestrictedMoveResize()) { // Make sure the titlebar isn't behind a restricted area. We don't need to restrict // the other directions. If not visible enough, move the window to the closest valid // point. We bruteforce this by slowly moving the window back to its previous position QRegion availableArea(workspace()->clientArea(FullArea, -1, 0)); // On the screen availableArea -= workspace()->restrictedMoveArea(desktop()); // Strut areas bool transposed = false; int requiredPixels; QRect bTitleRect = titleBarRect(transposed, requiredPixels); int lastVisiblePixels = -1; QRect lastTry = moveResizeGeometry(); bool titleFailed = false; for (;;) { const QRect titleRect(bTitleRect.translated(moveResizeGeometry().topLeft())); int visiblePixels = 0; int realVisiblePixels = 0; for (const QRect &rect : availableArea) { const QRect r = rect & titleRect; realVisiblePixels += r.width() * r.height(); if ((transposed && r.width() == titleRect.width()) || // Only the full size regions... (!transposed && r.height() == titleRect.height())) // ...prevents long slim areas visiblePixels += r.width() * r.height(); } if (visiblePixels >= requiredPixels) break; // We have reached a valid position if (realVisiblePixels <= lastVisiblePixels) { if (titleFailed && realVisiblePixels < lastVisiblePixels) break; // we won't become better else { if (!titleFailed) setMoveResizeGeometry(lastTry); titleFailed = true; } } lastVisiblePixels = realVisiblePixels; QRect moveResizeGeom = moveResizeGeometry(); lastTry = moveResizeGeom; // Not visible enough, move the window to the closest valid point. We bruteforce // this by slowly moving the window back to its previous position. // The geometry changes at up to two edges, the one with the title (if) shall take // precedence. The opposing edge has no impact on visiblePixels and only one of // the adjacent can alter at a time, ie. it's enough to ignore adjacent edges // if the title edge altered bool leftChanged = previousMoveResizeGeom.left() != moveResizeGeom.left(); bool rightChanged = previousMoveResizeGeom.right() != moveResizeGeom.right(); bool topChanged = previousMoveResizeGeom.top() != moveResizeGeom.top(); bool btmChanged = previousMoveResizeGeom.bottom() != moveResizeGeom.bottom(); auto fixChangedState = [titleFailed](bool &major, bool &counter, bool &ad1, bool &ad2) { counter = false; if (titleFailed) major = false; if (major) ad1 = ad2 = false; }; switch (titlebarPosition()) { default: case PositionTop: fixChangedState(topChanged, btmChanged, leftChanged, rightChanged); break; case PositionLeft: fixChangedState(leftChanged, rightChanged, topChanged, btmChanged); break; case PositionBottom: fixChangedState(btmChanged, topChanged, leftChanged, rightChanged); break; case PositionRight: fixChangedState(rightChanged, leftChanged, topChanged, btmChanged); break; } if (topChanged) moveResizeGeom.setTop(moveResizeGeom.y() + sign(previousMoveResizeGeom.y() - moveResizeGeom.y())); else if (leftChanged) moveResizeGeom.setLeft(moveResizeGeom.x() + sign(previousMoveResizeGeom.x() - moveResizeGeom.x())); else if (btmChanged) moveResizeGeom.setBottom(moveResizeGeom.bottom() + sign(previousMoveResizeGeom.bottom() - moveResizeGeom.bottom())); else if (rightChanged) moveResizeGeom.setRight(moveResizeGeom.right() + sign(previousMoveResizeGeom.right() - moveResizeGeom.right())); else break; // no position changed - that's certainly not good setMoveResizeGeometry(moveResizeGeom); } } // Always obey size hints, even when in "unrestricted" mode QSize size = constrainFrameSize(moveResizeGeometry().size(), sizeMode); // the new topleft and bottomright corners (after checking size constrains), if they'll be needed topleft = QPoint(moveResizeGeometry().right() - size.width() + 1, moveResizeGeometry().bottom() - size.height() + 1); bottomright = QPoint(moveResizeGeometry().left() + size.width() - 1, moveResizeGeometry().top() + size.height() - 1); orig = moveResizeGeometry(); // if aspect ratios are specified, both dimensions may change. // Therefore grow to the right/bottom if needed. // TODO it should probably obey gravity rather than always using right/bottom ? if (sizeMode == SizeModeFixedH) orig.setRight(bottomright.x()); else if (sizeMode == SizeModeFixedW) orig.setBottom(bottomright.y()); calculateMoveResizeGeom(); if (moveResizeGeometry().size() != previousMoveResizeGeom.size()) update = true; } else if (isMove()) { Q_ASSERT(mode == PositionCenter); if (!isMovable()) { // isMovableAcrossScreens() must have been true to get here // Special moving of maximized windows on Xinerama screens int screen = screens()->number(globalPos); if (isFullScreen()) setMoveResizeGeometry(workspace()->clientArea(FullScreenArea, screen, 0)); else { QRect moveResizeGeom = workspace()->clientArea(MaximizeArea, screen, 0); QSize adjSize = constrainFrameSize(moveResizeGeom.size(), SizeModeMax); if (adjSize != moveResizeGeom.size()) { QRect r(moveResizeGeom); moveResizeGeom.setSize(adjSize); moveResizeGeom.moveCenter(r.center()); } setMoveResizeGeometry(moveResizeGeom); } } else { // first move, then snap, then check bounds QRect moveResizeGeom = moveResizeGeometry(); moveResizeGeom.moveTopLeft(topleft); moveResizeGeom.moveTopLeft(workspace()->adjustClientPosition(this, moveResizeGeom.topLeft(), isUnrestrictedMoveResize())); setMoveResizeGeometry(moveResizeGeom); if (!isUnrestrictedMoveResize()) { const QRegion strut = workspace()->restrictedMoveArea(desktop()); // Strut areas QRegion availableArea(workspace()->clientArea(FullArea, -1, 0)); // On the screen availableArea -= strut; // Strut areas bool transposed = false; int requiredPixels; QRect bTitleRect = titleBarRect(transposed, requiredPixels); for (;;) { QRect moveResizeGeom = moveResizeGeometry(); const QRect titleRect(bTitleRect.translated(moveResizeGeom.topLeft())); int visiblePixels = 0; for (const QRect &rect : availableArea) { const QRect r = rect & titleRect; if ((transposed && r.width() == titleRect.width()) || // Only the full size regions... (!transposed && r.height() == titleRect.height())) // ...prevents long slim areas visiblePixels += r.width() * r.height(); } if (visiblePixels >= requiredPixels) break; // We have reached a valid position // (esp.) if there're more screens with different struts (panels) it the titlebar // will be movable outside the movearea (covering one of the panels) until it // crosses the panel "too much" (not enough visiblePixels) and then stucks because // it's usually only pushed by 1px to either direction // so we first check whether we intersect suc strut and move the window below it // immediately (it's still possible to hit the visiblePixels >= titlebarArea break // by moving the window slightly downwards, but it won't stuck) // see bug #274466 // and bug #301805 for why we can't just match the titlearea against the screen if (screens()->count() > 1) { // optimization // TODO: could be useful on partial screen struts (half-width panels etc.) int newTitleTop = -1; for (const QRect &r : strut) { if (r.top() == 0 && r.width() > r.height() && // "top panel" r.intersects(moveResizeGeom) && moveResizeGeom.top() < r.bottom()) { newTitleTop = r.bottom() + 1; break; } } if (newTitleTop > -1) { moveResizeGeom.moveTop(newTitleTop); // invalid position, possibly on screen change setMoveResizeGeometry(moveResizeGeom); break; } } int dx = sign(previousMoveResizeGeom.x() - moveResizeGeom.x()), dy = sign(previousMoveResizeGeom.y() - moveResizeGeom.y()); if (visiblePixels && dx) // means there's no full width cap -> favor horizontally dy = 0; else if (dy) dx = 0; // Move it back moveResizeGeom.translate(dx, dy); setMoveResizeGeometry(moveResizeGeom); if (moveResizeGeom == previousMoveResizeGeom) { break; // Prevent lockup } } } } if (moveResizeGeometry().topLeft() != previousMoveResizeGeom.topLeft()) update = true; } else abort(); if (!update) return; if (isResize() && !haveResizeEffect()) { doResizeSync(); } else performMoveResize(); if (isMove()) { ScreenEdges::self()->check(globalPos, QDateTime::fromMSecsSinceEpoch(xTime(), Qt::UTC)); } } void AbstractClient::performMoveResize() { const QRect &moveResizeGeom = moveResizeGeometry(); if (isMove() || (isResize() && !haveResizeEffect())) { setFrameGeometry(moveResizeGeom); } doPerformMoveResize(); positionGeometryTip(); emit clientStepUserMovedResized(this, moveResizeGeom); } bool AbstractClient::hasStrut() const { return false; } void AbstractClient::setupWindowManagementInterface() { if (m_windowManagementInterface) { // already setup return; } if (!waylandServer() || !surface()) { return; } if (!waylandServer()->windowManagement()) { return; } using namespace KWaylandServer; auto w = waylandServer()->windowManagement()->createWindow(waylandServer()->windowManagement()); w->setTitle(caption()); w->setVirtualDesktop(isOnAllDesktops() ? 0 : desktop() - 1); w->setActive(isActive()); w->setFullscreen(isFullScreen()); w->setKeepAbove(keepAbove()); w->setKeepBelow(keepBelow()); w->setMaximized(maximizeMode() == KWin::MaximizeFull); w->setMinimized(isMinimized()); w->setOnAllDesktops(isOnAllDesktops()); w->setDemandsAttention(isDemandingAttention()); w->setCloseable(isCloseable()); w->setMaximizeable(isMaximizable()); w->setMinimizeable(isMinimizable()); w->setFullscreenable(isFullScreenable()); w->setApplicationMenuPaths(applicationMenuServiceName(), applicationMenuObjectPath()); w->setIcon(icon()); auto updateAppId = [this, w] { w->setAppId(QString::fromUtf8(m_desktopFileName.isEmpty() ? resourceClass() : m_desktopFileName)); }; updateAppId(); w->setSkipTaskbar(skipTaskbar()); w->setSkipSwitcher(skipSwitcher()); w->setPid(pid()); w->setShadeable(isShadeable()); w->setShaded(isShade()); w->setResizable(isResizable()); w->setMovable(isMovable()); w->setVirtualDesktopChangeable(true); // FIXME Matches X11Client::actionSupported(), but both should be implemented. w->setParentWindow(transientFor() ? transientFor()->windowManagementInterface() : nullptr); w->setGeometry(frameGeometry()); connect(this, &AbstractClient::skipTaskbarChanged, w, [w, this] { w->setSkipTaskbar(skipTaskbar()); } ); connect(this, &AbstractClient::skipSwitcherChanged, w, [w, this] { w->setSkipSwitcher(skipSwitcher()); } ); connect(this, &AbstractClient::captionChanged, w, [w, this] { w->setTitle(caption()); }); connect(this, &AbstractClient::activeChanged, w, [w, this] { w->setActive(isActive()); }); connect(this, &AbstractClient::fullScreenChanged, w, [w, this] { w->setFullscreen(isFullScreen()); }); connect(this, &AbstractClient::keepAboveChanged, w, &PlasmaWindowInterface::setKeepAbove); connect(this, &AbstractClient::keepBelowChanged, w, &PlasmaWindowInterface::setKeepBelow); connect(this, &AbstractClient::minimizedChanged, w, [w, this] { w->setMinimized(isMinimized()); }); connect(this, static_cast(&AbstractClient::clientMaximizedStateChanged), w, [w] (KWin::AbstractClient *c, MaximizeMode mode) { Q_UNUSED(c); w->setMaximized(mode == KWin::MaximizeFull); } ); connect(this, &AbstractClient::demandsAttentionChanged, w, [w, this] { w->setDemandsAttention(isDemandingAttention()); }); connect(this, &AbstractClient::iconChanged, w, [w, this] { w->setIcon(icon()); } ); connect(this, &AbstractClient::windowClassChanged, w, updateAppId); connect(this, &AbstractClient::desktopFileNameChanged, w, updateAppId); connect(this, &AbstractClient::shadeChanged, w, [w, this] { w->setShaded(isShade()); }); connect(this, &AbstractClient::transientChanged, w, [w, this] { w->setParentWindow(transientFor() ? transientFor()->windowManagementInterface() : nullptr); } ); connect(this, &AbstractClient::frameGeometryChanged, w, [w, this] { w->setGeometry(frameGeometry()); } ); connect(this, &AbstractClient::applicationMenuChanged, w, [w, this] { w->setApplicationMenuPaths(applicationMenuServiceName(), applicationMenuObjectPath()); } ); connect(w, &PlasmaWindowInterface::closeRequested, this, [this] { closeWindow(); }); connect(w, &PlasmaWindowInterface::moveRequested, this, [this] { Cursors::self()->mouse()->setPos(frameGeometry().center()); performMouseCommand(Options::MouseMove, Cursors::self()->mouse()->pos()); } ); connect(w, &PlasmaWindowInterface::resizeRequested, this, [this] { Cursors::self()->mouse()->setPos(frameGeometry().bottomRight()); performMouseCommand(Options::MouseResize, Cursors::self()->mouse()->pos()); } ); connect(w, &PlasmaWindowInterface::virtualDesktopRequested, this, [this] (quint32 desktop) { workspace()->sendClientToDesktop(this, desktop + 1, true); } ); connect(w, &PlasmaWindowInterface::fullscreenRequested, this, [this] (bool set) { setFullScreen(set, false); } ); connect(w, &PlasmaWindowInterface::minimizedRequested, this, [this] (bool set) { if (set) { minimize(); } else { unminimize(); } } ); connect(w, &PlasmaWindowInterface::maximizedRequested, this, [this] (bool set) { maximize(set ? MaximizeFull : MaximizeRestore); } ); connect(w, &PlasmaWindowInterface::keepAboveRequested, this, [this] (bool set) { setKeepAbove(set); } ); connect(w, &PlasmaWindowInterface::keepBelowRequested, this, [this] (bool set) { setKeepBelow(set); } ); connect(w, &PlasmaWindowInterface::demandsAttentionRequested, this, [this] (bool set) { demandAttention(set); } ); connect(w, &PlasmaWindowInterface::activeRequested, this, [this] (bool set) { if (set) { workspace()->activateClient(this, true); } } ); connect(w, &PlasmaWindowInterface::shadedRequested, this, [this] (bool set) { setShade(set); } ); for (const auto vd : m_desktops) { w->addPlasmaVirtualDesktop(vd->id()); } //this is only for the legacy connect(this, &AbstractClient::desktopChanged, w, [w, this] { if (isOnAllDesktops()) { w->setOnAllDesktops(true); return; } w->setVirtualDesktop(desktop() - 1); w->setOnAllDesktops(false); } ); //Plasma Virtual desktop management //show/hide when the window enters/exits from desktop connect(w, &PlasmaWindowInterface::enterPlasmaVirtualDesktopRequested, this, [this] (const QString &desktopId) { VirtualDesktop *vd = VirtualDesktopManager::self()->desktopForId(desktopId.toUtf8()); if (vd) { enterDesktop(vd); } } ); connect(w, &PlasmaWindowInterface::enterNewPlasmaVirtualDesktopRequested, this, [this] () { VirtualDesktopManager::self()->setCount(VirtualDesktopManager::self()->count() + 1); enterDesktop(VirtualDesktopManager::self()->desktops().last()); } ); connect(w, &PlasmaWindowInterface::leavePlasmaVirtualDesktopRequested, this, [this] (const QString &desktopId) { VirtualDesktop *vd = VirtualDesktopManager::self()->desktopForId(desktopId.toUtf8()); if (vd) { leaveDesktop(vd); } } ); m_windowManagementInterface = w; } void AbstractClient::destroyWindowManagementInterface() { if (m_windowManagementInterface) { m_windowManagementInterface->unmap(); m_windowManagementInterface = nullptr; } } Options::MouseCommand AbstractClient::getMouseCommand(Qt::MouseButton button, bool *handled) const { *handled = false; if (button == Qt::NoButton) { return Options::MouseNothing; } if (isActive()) { if (options->isClickRaise()) { *handled = true; return Options::MouseActivateRaiseAndPassClick; } } else { *handled = true; switch (button) { case Qt::LeftButton: return options->commandWindow1(); case Qt::MiddleButton: return options->commandWindow2(); case Qt::RightButton: return options->commandWindow3(); default: // all other buttons pass Activate & Pass Client return Options::MouseActivateAndPassClick; } } return Options::MouseNothing; } Options::MouseCommand AbstractClient::getWheelCommand(Qt::Orientation orientation, bool *handled) const { *handled = false; if (orientation != Qt::Vertical) { return Options::MouseNothing; } if (!isActive()) { *handled = true; return options->commandWindowWheel(); } return Options::MouseNothing; } bool AbstractClient::performMouseCommand(Options::MouseCommand cmd, const QPoint &globalPos) { bool replay = false; switch(cmd) { case Options::MouseRaise: workspace()->raiseClient(this); break; case Options::MouseLower: { workspace()->lowerClient(this); // used to be activateNextClient(this), then topClientOnDesktop // since this is a mouseOp it's however safe to use the client under the mouse instead if (isActive() && options->focusPolicyIsReasonable()) { AbstractClient *next = workspace()->clientUnderMouse(screen()); if (next && next != this) workspace()->requestFocus(next, false); } break; } case Options::MouseOperationsMenu: if (isActive() && options->isClickRaise()) autoRaise(); workspace()->showWindowMenu(QRect(globalPos, globalPos), this); break; case Options::MouseToggleRaiseAndLower: workspace()->raiseOrLowerClient(this); break; case Options::MouseActivateAndRaise: { replay = isActive(); // for clickraise mode bool mustReplay = !rules()->checkAcceptFocus(acceptsFocus()); if (mustReplay) { auto it = workspace()->stackingOrder().constEnd(), begin = workspace()->stackingOrder().constBegin(); while (mustReplay && --it != begin && *it != this) { AbstractClient *c = qobject_cast(*it); if (!c || (c->keepAbove() && !keepAbove()) || (keepBelow() && !c->keepBelow())) continue; // can never raise above "it" mustReplay = !(c->isOnCurrentDesktop() && c->isOnCurrentActivity() && c->frameGeometry().intersects(frameGeometry())); } } workspace()->takeActivity(this, Workspace::ActivityFocus | Workspace::ActivityRaise); screens()->setCurrent(globalPos); replay = replay || mustReplay; break; } case Options::MouseActivateAndLower: workspace()->requestFocus(this); workspace()->lowerClient(this); screens()->setCurrent(globalPos); replay = replay || !rules()->checkAcceptFocus(acceptsFocus()); break; case Options::MouseActivate: replay = isActive(); // for clickraise mode workspace()->takeActivity(this, Workspace::ActivityFocus); screens()->setCurrent(globalPos); replay = replay || !rules()->checkAcceptFocus(acceptsFocus()); break; case Options::MouseActivateRaiseAndPassClick: workspace()->takeActivity(this, Workspace::ActivityFocus | Workspace::ActivityRaise); screens()->setCurrent(globalPos); replay = true; break; case Options::MouseActivateAndPassClick: workspace()->takeActivity(this, Workspace::ActivityFocus); screens()->setCurrent(globalPos); replay = true; break; case Options::MouseMaximize: maximize(MaximizeFull); break; case Options::MouseRestore: maximize(MaximizeRestore); break; case Options::MouseMinimize: minimize(); break; case Options::MouseAbove: { StackingUpdatesBlocker blocker(workspace()); if (keepBelow()) setKeepBelow(false); else setKeepAbove(true); break; } case Options::MouseBelow: { StackingUpdatesBlocker blocker(workspace()); if (keepAbove()) setKeepAbove(false); else setKeepBelow(true); break; } case Options::MousePreviousDesktop: workspace()->windowToPreviousDesktop(this); break; case Options::MouseNextDesktop: workspace()->windowToNextDesktop(this); break; case Options::MouseOpacityMore: if (!isDesktop()) // No point in changing the opacity of the desktop setOpacity(qMin(opacity() + 0.1, 1.0)); break; case Options::MouseOpacityLess: if (!isDesktop()) // No point in changing the opacity of the desktop setOpacity(qMax(opacity() - 0.1, 0.1)); break; case Options::MouseClose: closeWindow(); break; case Options::MouseActivateRaiseAndMove: case Options::MouseActivateRaiseAndUnrestrictedMove: workspace()->raiseClient(this); workspace()->requestFocus(this); screens()->setCurrent(globalPos); // fallthrough case Options::MouseMove: case Options::MouseUnrestrictedMove: { if (!isMovableAcrossScreens()) break; if (isMoveResize()) finishMoveResize(false); setMoveResizePointerMode(PositionCenter); setMoveResizePointerButtonDown(true); setMoveOffset(QPoint(globalPos.x() - x(), globalPos.y() - y())); // map from global setInvertedMoveOffset(rect().bottomRight() - moveOffset()); setUnrestrictedMoveResize((cmd == Options::MouseActivateRaiseAndUnrestrictedMove || cmd == Options::MouseUnrestrictedMove)); if (!startMoveResize()) setMoveResizePointerButtonDown(false); updateCursor(); break; } case Options::MouseResize: case Options::MouseUnrestrictedResize: { if (!isResizable() || isShade()) break; if (isMoveResize()) finishMoveResize(false); setMoveResizePointerButtonDown(true); const QPoint moveOffset = QPoint(globalPos.x() - x(), globalPos.y() - y()); // map from global setMoveOffset(moveOffset); int x = moveOffset.x(), y = moveOffset.y(); bool left = x < width() / 3; bool right = x >= 2 * width() / 3; bool top = y < height() / 3; bool bot = y >= 2 * height() / 3; Position mode; if (top) mode = left ? PositionTopLeft : (right ? PositionTopRight : PositionTop); else if (bot) mode = left ? PositionBottomLeft : (right ? PositionBottomRight : PositionBottom); else mode = (x < width() / 2) ? PositionLeft : PositionRight; setMoveResizePointerMode(mode); setInvertedMoveOffset(rect().bottomRight() - moveOffset); setUnrestrictedMoveResize((cmd == Options::MouseUnrestrictedResize)); if (!startMoveResize()) setMoveResizePointerButtonDown(false); updateCursor(); break; } case Options::MouseShade: toggleShade(); cancelShadeHoverTimer(); break; case Options::MouseSetShade: setShade(ShadeNormal); cancelShadeHoverTimer(); break; case Options::MouseUnsetShade: setShade(ShadeNone); cancelShadeHoverTimer(); break; case Options::MouseNothing: default: replay = true; break; } return replay; } void AbstractClient::setTransientFor(AbstractClient *transientFor) { if (transientFor == this) { // cannot be transient for one self return; } if (m_transientFor == transientFor) { return; } m_transientFor = transientFor; emit transientChanged(); } const AbstractClient *AbstractClient::transientFor() const { return m_transientFor; } AbstractClient *AbstractClient::transientFor() { return m_transientFor; } bool AbstractClient::hasTransientPlacementHint() const { return false; } QRect AbstractClient::transientPlacement(const QRect &bounds) const { Q_UNUSED(bounds); Q_UNREACHABLE(); return QRect(); } bool AbstractClient::hasTransient(const AbstractClient *c, bool indirect) const { Q_UNUSED(indirect); return c->transientFor() == this; } QList< AbstractClient* > AbstractClient::mainClients() const { if (const AbstractClient *t = transientFor()) { return QList{const_cast< AbstractClient* >(t)}; } return QList(); } QList AbstractClient::allMainClients() const { auto result = mainClients(); foreach (const auto *cl, result) { result += cl->allMainClients(); } return result; } void AbstractClient::setModal(bool m) { // Qt-3.2 can have even modal normal windows :( if (m_modal == m) return; m_modal = m; emit modalChanged(); // Changing modality for a mapped window is weird (?) // _NET_WM_STATE_MODAL should possibly rather be _NET_WM_WINDOW_TYPE_MODAL_DIALOG } bool AbstractClient::isModal() const { return m_modal; } void AbstractClient::addTransient(AbstractClient *cl) { Q_ASSERT(!m_transients.contains(cl)); Q_ASSERT(cl != this); m_transients.append(cl); } void AbstractClient::removeTransient(AbstractClient *cl) { m_transients.removeAll(cl); if (cl->transientFor() == this) { cl->setTransientFor(nullptr); } } void AbstractClient::removeTransientFromList(AbstractClient *cl) { m_transients.removeAll(cl); } bool AbstractClient::isActiveFullScreen() const { if (!isFullScreen()) return false; const auto ac = workspace()->mostRecentlyActivatedClient(); // instead of activeClient() - avoids flicker // according to NETWM spec implementation notes suggests // "focused windows having state _NET_WM_STATE_FULLSCREEN" to be on the highest layer. // we'll also take the screen into account return ac && (ac == this || ac->screen() != screen()|| ac->allMainClients().contains(const_cast(this))); } #define BORDER(which) \ int AbstractClient::border##which() const \ { \ return isDecorated() ? decoration()->border##which() : 0; \ } BORDER(Bottom) BORDER(Left) BORDER(Right) BORDER(Top) #undef BORDER void AbstractClient::addRepaintDuringGeometryUpdates() { const QRect deco_rect = visibleRect(); addLayerRepaint(m_visibleRectBeforeGeometryUpdate); addLayerRepaint(deco_rect); // trigger repaint of window's new location m_visibleRectBeforeGeometryUpdate = deco_rect; } QRect AbstractClient::bufferGeometryBeforeUpdateBlocking() const { return m_bufferGeometryBeforeUpdateBlocking; } QRect AbstractClient::frameGeometryBeforeUpdateBlocking() const { return m_frameGeometryBeforeUpdateBlocking; } void AbstractClient::updateGeometryBeforeUpdateBlocking() { m_bufferGeometryBeforeUpdateBlocking = bufferGeometry(); m_frameGeometryBeforeUpdateBlocking = frameGeometry(); } void AbstractClient::doMove(int, int) { } void AbstractClient::updateInitialMoveResizeGeometry() { m_moveResize.initialGeometry = frameGeometry(); m_moveResize.geometry = m_moveResize.initialGeometry; m_moveResize.startScreen = screen(); } void AbstractClient::updateCursor() { Position m = moveResizePointerMode(); if (!isResizable() || isShade()) m = PositionCenter; CursorShape c = Qt::ArrowCursor; switch(m) { case PositionTopLeft: c = KWin::ExtendedCursor::SizeNorthWest; break; case PositionBottomRight: c = KWin::ExtendedCursor::SizeSouthEast; break; case PositionBottomLeft: c = KWin::ExtendedCursor::SizeSouthWest; break; case PositionTopRight: c = KWin::ExtendedCursor::SizeNorthEast; break; case PositionTop: c = KWin::ExtendedCursor::SizeNorth; break; case PositionBottom: c = KWin::ExtendedCursor::SizeSouth; break; case PositionLeft: c = KWin::ExtendedCursor::SizeWest; break; case PositionRight: c = KWin::ExtendedCursor::SizeEast; break; default: if (isMoveResize()) c = Qt::SizeAllCursor; else c = Qt::ArrowCursor; break; } if (c == m_moveResize.cursor) return; m_moveResize.cursor = c; emit moveResizeCursorChanged(c); } void AbstractClient::leaveMoveResize() { workspace()->setMoveResizeClient(nullptr); setMoveResize(false); if (ScreenEdges::self()->isDesktopSwitchingMovingClients()) ScreenEdges::self()->reserveDesktopSwitching(false, Qt::Vertical|Qt::Horizontal); if (isElectricBorderMaximizing()) { outline()->hide(); elevate(false); } } bool AbstractClient::s_haveResizeEffect = false; void AbstractClient::updateHaveResizeEffect() { s_haveResizeEffect = effects && static_cast(effects)->provides(Effect::Resize); } bool AbstractClient::doStartMoveResize() { return true; } +void AbstractClient::doFinishMoveResize() +{ +} + void AbstractClient::positionGeometryTip() { } void AbstractClient::doPerformMoveResize() { } bool AbstractClient::isWaitingForMoveResizeSync() const { return false; } void AbstractClient::doResizeSync() { } void AbstractClient::checkQuickTilingMaximizationZones(int xroot, int yroot) { QuickTileMode mode = QuickTileFlag::None; bool innerBorder = false; for (int i=0; i < screens()->count(); ++i) { if (!screens()->geometry(i).contains(QPoint(xroot, yroot))) continue; auto isInScreen = [i](const QPoint &pt) { for (int j = 0; j < screens()->count(); ++j) { if (j == i) continue; if (screens()->geometry(j).contains(pt)) { return true; } } return false; }; QRect area = workspace()->clientArea(MaximizeArea, QPoint(xroot, yroot), desktop()); if (options->electricBorderTiling()) { if (xroot <= area.x() + 20) { mode |= QuickTileFlag::Left; innerBorder = isInScreen(QPoint(area.x() - 1, yroot)); } else if (xroot >= area.x() + area.width() - 20) { mode |= QuickTileFlag::Right; innerBorder = isInScreen(QPoint(area.right() + 1, yroot)); } } if (mode != QuickTileMode(QuickTileFlag::None)) { if (yroot <= area.y() + area.height() * options->electricBorderCornerRatio()) mode |= QuickTileFlag::Top; else if (yroot >= area.y() + area.height() - area.height() * options->electricBorderCornerRatio()) mode |= QuickTileFlag::Bottom; } else if (options->electricBorderMaximize() && yroot <= area.y() + 5 && isMaximizable()) { mode = QuickTileFlag::Maximize; innerBorder = isInScreen(QPoint(xroot, area.y() - 1)); } break; // no point in checking other screens to contain this... "point"... } if (mode != electricBorderMode()) { setElectricBorderMode(mode); if (innerBorder) { if (!m_electricMaximizingDelay) { m_electricMaximizingDelay = new QTimer(this); m_electricMaximizingDelay->setInterval(250); m_electricMaximizingDelay->setSingleShot(true); connect(m_electricMaximizingDelay, &QTimer::timeout, [this]() { if (isMove()) setElectricBorderMaximizing(electricBorderMode() != QuickTileMode(QuickTileFlag::None)); }); } m_electricMaximizingDelay->start(); } else { setElectricBorderMaximizing(mode != QuickTileMode(QuickTileFlag::None)); } } } void AbstractClient::keyPressEvent(uint key_code) { if (!isMove() && !isResize()) return; bool is_control = key_code & Qt::CTRL; bool is_alt = key_code & Qt::ALT; key_code = key_code & ~Qt::KeyboardModifierMask; int delta = is_control ? 1 : is_alt ? 32 : 8; QPoint pos = Cursors::self()->mouse()->pos(); switch(key_code) { case Qt::Key_Left: pos.rx() -= delta; break; case Qt::Key_Right: pos.rx() += delta; break; case Qt::Key_Up: pos.ry() -= delta; break; case Qt::Key_Down: pos.ry() += delta; break; case Qt::Key_Space: case Qt::Key_Return: case Qt::Key_Enter: setMoveResizePointerButtonDown(false); finishMoveResize(false); updateCursor(); break; case Qt::Key_Escape: setMoveResizePointerButtonDown(false); finishMoveResize(true); updateCursor(); break; default: return; } Cursors::self()->mouse()->setPos(pos); } QSize AbstractClient::resizeIncrements() const { return QSize(1, 1); } void AbstractClient::dontMoveResize() { setMoveResizePointerButtonDown(false); stopDelayedMoveResize(); if (isMoveResize()) finishMoveResize(false); } AbstractClient::Position AbstractClient::mousePosition() const { if (isDecorated()) { switch (decoration()->sectionUnderMouse()) { case Qt::BottomLeftSection: return PositionBottomLeft; case Qt::BottomRightSection: return PositionBottomRight; case Qt::BottomSection: return PositionBottom; case Qt::LeftSection: return PositionLeft; case Qt::RightSection: return PositionRight; case Qt::TopSection: return PositionTop; case Qt::TopLeftSection: return PositionTopLeft; case Qt::TopRightSection: return PositionTopRight; default: return PositionCenter; } } return PositionCenter; } void AbstractClient::endMoveResize() { setMoveResizePointerButtonDown(false); stopDelayedMoveResize(); if (isMoveResize()) { finishMoveResize(false); setMoveResizePointerMode(mousePosition()); } updateCursor(); } void AbstractClient::createDecoration(const QRect &oldGeometry) { KDecoration2::Decoration *decoration = Decoration::DecorationBridge::self()->createDecoration(this); if (decoration) { QMetaObject::invokeMethod(decoration, "update", Qt::QueuedConnection); connect(decoration, &KDecoration2::Decoration::shadowChanged, this, &Toplevel::updateShadow); connect(decoration, &KDecoration2::Decoration::bordersChanged, this, [this]() { GeometryUpdatesBlocker blocker(this); const QRect oldGeometry = frameGeometry(); if (!isShade()) { checkWorkspacePosition(oldGeometry); } emit geometryShapeChanged(this, oldGeometry); }); } setDecoration(decoration); setFrameGeometry(QRect(oldGeometry.topLeft(), clientSizeToFrameSize(clientSize()))); emit geometryShapeChanged(this, oldGeometry); } void AbstractClient::destroyDecoration() { delete m_decoration.decoration; m_decoration.decoration = nullptr; } bool AbstractClient::decorationHasAlpha() const { if (!isDecorated() || decoration()->isOpaque()) { // either no decoration or decoration has alpha disabled return false; } return true; } void AbstractClient::triggerDecorationRepaint() { if (isDecorated()) { decoration()->update(); } } void AbstractClient::layoutDecorationRects(QRect &left, QRect &top, QRect &right, QRect &bottom) const { if (!isDecorated()) { return; } QRect r = decoration()->rect(); top = QRect(r.x(), r.y(), r.width(), borderTop()); bottom = QRect(r.x(), r.y() + r.height() - borderBottom(), r.width(), borderBottom()); left = QRect(r.x(), r.y() + top.height(), borderLeft(), r.height() - top.height() - bottom.height()); right = QRect(r.x() + r.width() - borderRight(), r.y() + top.height(), borderRight(), r.height() - top.height() - bottom.height()); } void AbstractClient::processDecorationMove(const QPoint &localPos, const QPoint &globalPos) { if (isMoveResizePointerButtonDown()) { handleMoveResize(localPos.x(), localPos.y(), globalPos.x(), globalPos.y()); return; } // TODO: handle modifiers Position newmode = mousePosition(); if (newmode != moveResizePointerMode()) { setMoveResizePointerMode(newmode); updateCursor(); } } bool AbstractClient::processDecorationButtonPress(QMouseEvent *event, bool ignoreMenu) { Options::MouseCommand com = Options::MouseNothing; bool active = isActive(); if (!wantsInput()) // we cannot be active, use it anyway active = true; // check whether it is a double click if (event->button() == Qt::LeftButton && titlebarPositionUnderMouse()) { if (m_decoration.doubleClickTimer.isValid()) { const qint64 interval = m_decoration.doubleClickTimer.elapsed(); m_decoration.doubleClickTimer.invalidate(); if (interval > QGuiApplication::styleHints()->mouseDoubleClickInterval()) { m_decoration.doubleClickTimer.start(); // expired -> new first click and pot. init } else { Workspace::self()->performWindowOperation(this, options->operationTitlebarDblClick()); dontMoveResize(); return false; } } else { m_decoration.doubleClickTimer.start(); // new first click and pot. init, could be invalidated by release - see below } } if (event->button() == Qt::LeftButton) com = active ? options->commandActiveTitlebar1() : options->commandInactiveTitlebar1(); else if (event->button() == Qt::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 { setMoveResizePointerMode(mousePosition()); setMoveResizePointerButtonDown(true); setMoveOffset(event->pos()); setInvertedMoveOffset(rect().bottomRight() - moveOffset()); setUnrestrictedMoveResize(false); startDelayedMoveResize(); updateCursor(); } // In the new API the decoration may process the menu action to display an inactive tab's menu. // If the event is unhandled then the core will create one for the active window in the group. if (!ignoreMenu || com != Options::MouseOperationsMenu) performMouseCommand(com, event->globalPos()); return !( // Return events that should be passed to the decoration in the new API com == Options::MouseRaise || com == Options::MouseOperationsMenu || com == Options::MouseActivateAndRaise || com == Options::MouseActivate || com == Options::MouseActivateRaiseAndPassClick || com == Options::MouseActivateAndPassClick || com == Options::MouseNothing); } void AbstractClient::processDecorationButtonRelease(QMouseEvent *event) { if (isDecorated()) { if (event->isAccepted() || !titlebarPositionUnderMouse()) { invalidateDecorationDoubleClickTimer(); // click was for the deco and shall not init a doubleclick } } if (event->buttons() == Qt::NoButton) { setMoveResizePointerButtonDown(false); stopDelayedMoveResize(); if (isMoveResize()) { finishMoveResize(false); setMoveResizePointerMode(mousePosition()); } updateCursor(); } } void AbstractClient::startDecorationDoubleClickTimer() { m_decoration.doubleClickTimer.start(); } void AbstractClient::invalidateDecorationDoubleClickTimer() { m_decoration.doubleClickTimer.invalidate(); } bool AbstractClient::providesContextHelp() const { return false; } void AbstractClient::showContextHelp() { } QPointer AbstractClient::decoratedClient() const { return m_decoration.client; } void AbstractClient::setDecoratedClient(QPointer< Decoration::DecoratedClientImpl > client) { m_decoration.client = client; } void AbstractClient::enterEvent(const QPoint &globalPos) { if (options->isShadeHover()) { cancelShadeHoverTimer(); startShadeHoverTimer(); } if (options->focusPolicy() == Options::ClickToFocus || workspace()->userActionsMenu()->isShown()) return; if (options->isAutoRaise() && !isDesktop() && !isDock() && workspace()->focusChangeEnabled() && globalPos != workspace()->focusMousePosition() && workspace()->topClientOnDesktop(VirtualDesktopManager::self()->current(), options->isSeparateScreenFocus() ? screen() : -1) != this) { startAutoRaise(); } if (isDesktop() || isDock()) return; // for FocusFollowsMouse, change focus only if the mouse has actually been moved, not if the focus // change came because of window changes (e.g. closing a window) - #92290 if (options->focusPolicy() != Options::FocusFollowsMouse || globalPos != workspace()->focusMousePosition()) { workspace()->requestDelayFocus(this); } } void AbstractClient::leaveEvent() { cancelAutoRaise(); workspace()->cancelDelayFocus(); cancelShadeHoverTimer(); startShadeUnhoverTimer(); // TODO: send hover leave to deco // TODO: handle Options::FocusStrictlyUnderMouse } QRect AbstractClient::iconGeometry() const { if (!windowManagementInterface() || !waylandServer()) { // window management interface is only available if the surface is mapped return QRect(); } int minDistance = INT_MAX; AbstractClient *candidatePanel = nullptr; QRect candidateGeom; for (auto i = windowManagementInterface()->minimizedGeometries().constBegin(), end = windowManagementInterface()->minimizedGeometries().constEnd(); i != end; ++i) { AbstractClient *client = waylandServer()->findClient(i.key()); if (!client) { continue; } const int distance = QPoint(client->pos() - pos()).manhattanLength(); if (distance < minDistance) { minDistance = distance; candidatePanel = client; candidateGeom = i.value(); } } if (!candidatePanel) { return QRect(); } return candidateGeom.translated(candidatePanel->pos()); } QRect AbstractClient::inputGeometry() const { if (isDecorated()) { return Toplevel::inputGeometry() + decoration()->resizeOnlyBorders(); } return Toplevel::inputGeometry(); } QRect AbstractClient::virtualKeyboardGeometry() const { return m_virtualKeyboardGeometry; } void AbstractClient::setVirtualKeyboardGeometry(const QRect &geo) { // No keyboard anymore if (geo.isEmpty() && !m_keyboardGeometryRestore.isEmpty()) { setFrameGeometry(m_keyboardGeometryRestore); m_keyboardGeometryRestore = QRect(); } else if (geo.isEmpty()) { return; // The keyboard has just been opened (rather than resized) save client geometry for a restore } else if (m_keyboardGeometryRestore.isEmpty()) { m_keyboardGeometryRestore = frameGeometry(); } m_virtualKeyboardGeometry = geo; // Don't resize Desktop and fullscreen windows if (isFullScreen() || isDesktop()) { return; } if (!geo.intersects(m_keyboardGeometryRestore)) { return; } const QRect availableArea = workspace()->clientArea(MaximizeArea, this); QRect newWindowGeometry = m_keyboardGeometryRestore; newWindowGeometry.moveBottom(geo.top()); newWindowGeometry.setTop(qMax(newWindowGeometry.top(), availableArea.top())); setFrameGeometry(newWindowGeometry); } 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 { const QString desktopFileName = QString::fromUtf8(m_desktopFileName); QString desktopFilePath; if (QDir::isAbsolutePath(desktopFileName)) { desktopFilePath = desktopFileName; } if (desktopFilePath.isEmpty()) { desktopFilePath = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, desktopFileName); } if (desktopFilePath.isEmpty()) { desktopFilePath = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, desktopFileName + QLatin1String(".desktop")); } KDesktopFile df(desktopFilePath); return df.readIcon(); } bool AbstractClient::hasApplicationMenu() const { return ApplicationMenu::self()->applicationMenuEnabled() && !m_applicationMenuServiceName.isEmpty() && !m_applicationMenuObjectPath.isEmpty(); } void AbstractClient::updateApplicationMenuServiceName(const QString &serviceName) { const bool old_hasApplicationMenu = hasApplicationMenu(); m_applicationMenuServiceName = serviceName; const bool new_hasApplicationMenu = hasApplicationMenu(); emit applicationMenuChanged(); if (old_hasApplicationMenu != new_hasApplicationMenu) { emit hasApplicationMenuChanged(new_hasApplicationMenu); } } void AbstractClient::updateApplicationMenuObjectPath(const QString &objectPath) { const bool old_hasApplicationMenu = hasApplicationMenu(); m_applicationMenuObjectPath = objectPath; const bool new_hasApplicationMenu = hasApplicationMenu(); emit applicationMenuChanged(); if (old_hasApplicationMenu != new_hasApplicationMenu) { emit hasApplicationMenuChanged(new_hasApplicationMenu); } } void AbstractClient::setApplicationMenuActive(bool applicationMenuActive) { if (m_applicationMenuActive != applicationMenuActive) { m_applicationMenuActive = applicationMenuActive; emit applicationMenuActiveChanged(applicationMenuActive); } } void AbstractClient::showApplicationMenu(int actionId) { if (isDecorated()) { decoration()->showApplicationMenu(actionId); } else { // we don't know where the application menu button will be, show it in the top left corner instead Workspace::self()->showApplicationMenu(QRect(), this, actionId); } } bool AbstractClient::unresponsive() const { return m_unresponsive; } void AbstractClient::setUnresponsive(bool unresponsive) { if (m_unresponsive != unresponsive) { m_unresponsive = unresponsive; emit unresponsiveChanged(m_unresponsive); emit captionChanged(); } } QString AbstractClient::shortcutCaptionSuffix() const { if (shortcut().isEmpty()) { return QString(); } return QLatin1String(" {") + shortcut().toString() + QLatin1Char('}'); } AbstractClient *AbstractClient::findClientWithSameCaption() const { auto fetchNameInternalPredicate = [this](const AbstractClient *cl) { return (!cl->isSpecialWindow() || cl->isToolbar()) && cl != this && cl->captionNormal() == captionNormal() && cl->captionSuffix() == captionSuffix(); }; return workspace()->findAbstractClient(fetchNameInternalPredicate); } QString AbstractClient::caption() const { QString cap = captionNormal() + captionSuffix(); if (unresponsive()) { cap += QLatin1String(" "); cap += i18nc("Application is not responding, appended to window title", "(Not Responding)"); } return cap; } void AbstractClient::removeRule(Rules* rule) { m_rules.remove(rule); } void AbstractClient::discardTemporaryRules() { m_rules.discardTemporary(); } void AbstractClient::evaluateWindowRules() { setupWindowRules(true); applyWindowRules(); } void AbstractClient::setOnActivities(QStringList newActivitiesList) { Q_UNUSED(newActivitiesList) } void AbstractClient::checkNoBorder() { setNoBorder(false); } bool AbstractClient::groupTransient() const { return false; } const Group *AbstractClient::group() const { return nullptr; } Group *AbstractClient::group() { return nullptr; } bool AbstractClient::isInternal() const { return false; } bool AbstractClient::supportsWindowRules() const { return true; } QMargins AbstractClient::frameMargins() const { return QMargins(borderLeft(), borderTop(), borderRight(), borderBottom()); } QPoint AbstractClient::framePosToClientPos(const QPoint &point) const { return point + QPoint(borderLeft(), borderTop()); } QPoint AbstractClient::clientPosToFramePos(const QPoint &point) const { return point - QPoint(borderLeft(), borderTop()); } QSize AbstractClient::frameSizeToClientSize(const QSize &size) const { const int width = size.width() - borderLeft() - borderRight(); const int height = size.height() - borderTop() - borderBottom(); return QSize(width, height); } QSize AbstractClient::clientSizeToFrameSize(const QSize &size) const { const int width = size.width() + borderLeft() + borderRight(); const int height = size.height() + borderTop() + borderBottom(); return QSize(width, height); } QRect AbstractClient::frameRectToClientRect(const QRect &rect) const { const QPoint position = framePosToClientPos(rect.topLeft()); const QSize size = frameSizeToClientSize(rect.size()); return QRect(position, size); } QRect AbstractClient::clientRectToFrameRect(const QRect &rect) const { const QPoint position = clientPosToFramePos(rect.topLeft()); const QSize size = clientSizeToFrameSize(rect.size()); return QRect(position, size); } void AbstractClient::setElectricBorderMode(QuickTileMode mode) { if (mode != QuickTileMode(QuickTileFlag::Maximize)) { // sanitize the mode, ie. simplify "invalid" combinations if ((mode & QuickTileFlag::Horizontal) == QuickTileMode(QuickTileFlag::Horizontal)) mode &= ~QuickTileMode(QuickTileFlag::Horizontal); if ((mode & QuickTileFlag::Vertical) == QuickTileMode(QuickTileFlag::Vertical)) mode &= ~QuickTileMode(QuickTileFlag::Vertical); } m_electricMode = mode; } void AbstractClient::setElectricBorderMaximizing(bool maximizing) { m_electricMaximizing = maximizing; if (maximizing) outline()->show(electricBorderMaximizeGeometry(Cursors::self()->mouse()->pos(), desktop()), moveResizeGeometry()); else outline()->hide(); elevate(maximizing); } QRect AbstractClient::electricBorderMaximizeGeometry(QPoint pos, int desktop) { if (electricBorderMode() == QuickTileMode(QuickTileFlag::Maximize)) { if (maximizeMode() == MaximizeFull) return geometryRestore(); else return workspace()->clientArea(MaximizeArea, pos, desktop); } QRect ret = workspace()->clientArea(MaximizeArea, pos, desktop); if (electricBorderMode() & QuickTileFlag::Left) ret.setRight(ret.left()+ret.width()/2 - 1); else if (electricBorderMode() & QuickTileFlag::Right) ret.setLeft(ret.right()-(ret.width()-ret.width()/2) + 1); if (electricBorderMode() & QuickTileFlag::Top) ret.setBottom(ret.top()+ret.height()/2 - 1); else if (electricBorderMode() & QuickTileFlag::Bottom) ret.setTop(ret.bottom()-(ret.height()-ret.height()/2) + 1); return ret; } void AbstractClient::setQuickTileMode(QuickTileMode mode, bool keyboard) { // Only allow quick tile on a regular window. if (!isResizable()) { return; } workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event GeometryUpdatesBlocker blocker(this); if (mode == QuickTileMode(QuickTileFlag::Maximize)) { m_quickTileMode = int(QuickTileFlag::None); if (maximizeMode() == MaximizeFull) { setMaximize(false, false); } else { QRect prev_geom_restore = geometryRestore(); // setMaximize() would set moveResizeGeom as geom_restore m_quickTileMode = int(QuickTileFlag::Maximize); setMaximize(true, true); QRect clientArea = workspace()->clientArea(MaximizeArea, this); if (frameGeometry().top() != clientArea.top()) { QRect r(frameGeometry()); r.moveTop(clientArea.top()); setFrameGeometry(r); } setGeometryRestore(prev_geom_restore); } emit quickTileModeChanged(); return; } // sanitize the mode, ie. simplify "invalid" combinations if ((mode & QuickTileFlag::Horizontal) == QuickTileMode(QuickTileFlag::Horizontal)) mode &= ~QuickTileMode(QuickTileFlag::Horizontal); if ((mode & QuickTileFlag::Vertical) == QuickTileMode(QuickTileFlag::Vertical)) mode &= ~QuickTileMode(QuickTileFlag::Vertical); setElectricBorderMode(mode); // used by ::electricBorderMaximizeGeometry(.) // restore from maximized so that it is possible to tile maximized windows with one hit or by dragging if (maximizeMode() != MaximizeRestore) { if (mode != QuickTileMode(QuickTileFlag::None)) { // decorations may turn off some borders when tiled const ForceGeometry_t geom_mode = isDecorated() ? ForceGeometrySet : NormalGeometrySet; m_quickTileMode = int(QuickTileFlag::None); // Temporary, so the maximize code doesn't get all confused setMaximize(false, false); setFrameGeometry(electricBorderMaximizeGeometry(keyboard ? frameGeometry().center() : Cursors::self()->mouse()->pos(), desktop()), geom_mode); // Store the mode change m_quickTileMode = mode; } else { m_quickTileMode = mode; setMaximize(false, false); } emit quickTileModeChanged(); return; } if (mode != QuickTileMode(QuickTileFlag::None)) { QPoint whichScreen = keyboard ? frameGeometry().center() : Cursors::self()->mouse()->pos(); // If trying to tile to the side that the window is already tiled to move the window to the next // screen if it exists, otherwise toggle the mode (set QuickTileFlag::None) if (quickTileMode() == mode) { const int numScreens = screens()->count(); const int curScreen = screen(); int nextScreen = curScreen; QVarLengthArray screens(numScreens); for (int i = 0; i < numScreens; ++i) // Cache screens[i] = Screens::self()->geometry(i); for (int i = 0; i < numScreens; ++i) { if (i == curScreen) continue; if (screens[i].bottom() <= screens[curScreen].top() || screens[i].top() >= screens[curScreen].bottom()) continue; // not in horizontal line const int x = screens[i].center().x(); if ((mode & QuickTileFlag::Horizontal) == QuickTileMode(QuickTileFlag::Left)) { if (x >= screens[curScreen].center().x() || (curScreen != nextScreen && x <= screens[nextScreen].center().x())) continue; // not left of current or more left then found next } else if ((mode & QuickTileFlag::Horizontal) == QuickTileMode(QuickTileFlag::Right)) { if (x <= screens[curScreen].center().x() || (curScreen != nextScreen && x >= screens[nextScreen].center().x())) continue; // not right of current or more right then found next } nextScreen = i; } if (nextScreen == curScreen) { mode = QuickTileFlag::None; // No other screens, toggle tiling } else { // Move to other screen setFrameGeometry(geometryRestore().translated(screens[nextScreen].topLeft() - screens[curScreen].topLeft())); whichScreen = screens[nextScreen].center(); // Swap sides if (mode & QuickTileFlag::Horizontal) { mode = (~mode & QuickTileFlag::Horizontal) | (mode & QuickTileFlag::Vertical); } } setElectricBorderMode(mode); // used by ::electricBorderMaximizeGeometry(.) } else if (quickTileMode() == QuickTileMode(QuickTileFlag::None)) { // Not coming out of an existing tile, not shifting monitors, we're setting a brand new tile. // Store geometry first, so we can go out of this tile later. setGeometryRestore(frameGeometry()); } if (mode != QuickTileMode(QuickTileFlag::None)) { m_quickTileMode = mode; // decorations may turn off some borders when tiled const ForceGeometry_t geom_mode = isDecorated() ? ForceGeometrySet : NormalGeometrySet; // Temporary, so the maximize code doesn't get all confused m_quickTileMode = int(QuickTileFlag::None); setFrameGeometry(electricBorderMaximizeGeometry(whichScreen, desktop()), geom_mode); } // Store the mode change m_quickTileMode = mode; } if (mode == QuickTileMode(QuickTileFlag::None)) { m_quickTileMode = int(QuickTileFlag::None); // Untiling, so just restore geometry, and we're done. if (!geometryRestore().isValid()) // invalid if we started maximized and wait for placement setGeometryRestore(frameGeometry()); // decorations may turn off some borders when tiled const ForceGeometry_t geom_mode = isDecorated() ? ForceGeometrySet : NormalGeometrySet; setFrameGeometry(geometryRestore(), geom_mode); checkWorkspacePosition(); // Just in case it's a different screen } 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 = frameGeometry(); QRect newGeom = oldGeom; // move the window to have the same relative position to the center of the screen // (i.e. one near the middle of the right edge will also end up near the middle of the right edge) QPoint center = newGeom.center() - oldScreenArea.center(); center.setX(center.x() * screenArea.width() / oldScreenArea.width()); center.setY(center.y() * screenArea.height() / oldScreenArea.height()); center += screenArea.center(); newGeom.moveCenter(center); setFrameGeometry(newGeom); // If the window was inside the old screen area, explicitly make sure its inside also the new screen area. // Calling checkWorkspacePosition() should ensure that, but when moving to a small screen the window could // be big enough to overlap outside of the new screen area, making struts from other screens come into effect, // which could alter the resulting geometry. if (oldScreenArea.contains(oldGeom)) { keepInArea(screenArea); } // align geom_restore - checkWorkspacePosition operates on it setGeometryRestore(frameGeometry()); checkWorkspacePosition(oldGeom); // re-align geom_restore to constrained geometry setGeometryRestore(frameGeometry()); // finally reset special states // NOTICE that MaximizeRestore/QuickTileFlag::None checks are required. // eg. setting QuickTileFlag::None would break maximization if (maxMode != MaximizeRestore) maximize(maxMode); if (qtMode != QuickTileMode(QuickTileFlag::None) && qtMode != quickTileMode()) setQuickTileMode(qtMode, true); auto tso = workspace()->ensureStackingOrder(transients()); for (auto it = tso.constBegin(), end = tso.constEnd(); it != end; ++it) (*it)->sendToScreen(newScreen); } void AbstractClient::checkWorkspacePosition(QRect oldGeometry, int oldDesktop, QRect oldClientGeometry) { enum { Left = 0, Top, Right, Bottom }; const int border[4] = { borderLeft(), borderTop(), borderRight(), borderBottom() }; if( !oldGeometry.isValid()) oldGeometry = frameGeometry(); if( oldDesktop == -2 ) oldDesktop = desktop(); if (!oldClientGeometry.isValid()) oldClientGeometry = oldGeometry.adjusted(border[Left], border[Top], -border[Right], -border[Bottom]); if (isDesktop()) return; if (isFullScreen()) { QRect area = workspace()->clientArea(FullScreenArea, this); if (frameGeometry() != area) setFrameGeometry(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 = frameGeometry(); checkOffscreenPosition(&geom, screenArea); setFrameGeometry(geom); return; } if (quickTileMode() != QuickTileMode(QuickTileFlag::None)) { setFrameGeometry(electricBorderMaximizeGeometry(frameGeometry().center(), desktop())); return; } // this can be true only if this window was mapped before KWin // was started - in such case, don't adjust position to workarea, // because the window already had its position, and if a window // with a strut altering the workarea would be managed in initialization // after this one, this window would be moved if (!workspace() || workspace()->initializing()) return; // If the window was touching an edge before but not now move it so it is again. // Old and new maximums have different starting values so windows on the screen // edge will move when a new strut is placed on the edge. QRect oldScreenArea; if( workspace()->inUpdateClientArea()) { // we need to find the screen area as it was before the change oldScreenArea = QRect( 0, 0, workspace()->oldDisplayWidth(), workspace()->oldDisplayHeight()); int distance = INT_MAX; foreach(const QRect &r, workspace()->previousScreenSizes()) { int d = r.contains( oldGeometry.center()) ? 0 : ( r.center() - oldGeometry.center()).manhattanLength(); if( d < distance ) { distance = d; oldScreenArea = r; } } } else { oldScreenArea = workspace()->clientArea(ScreenArea, oldGeometry.center(), oldDesktop); } const QRect oldGeomTall = QRect(oldGeometry.x(), oldScreenArea.y(), oldGeometry.width(), oldScreenArea.height()); // Full screen height const QRect oldGeomWide = QRect(oldScreenArea.x(), oldGeometry.y(), oldScreenArea.width(), oldGeometry.height()); // Full screen width int oldTopMax = oldScreenArea.y(); int oldRightMax = oldScreenArea.x() + oldScreenArea.width(); int oldBottomMax = oldScreenArea.y() + oldScreenArea.height(); int oldLeftMax = oldScreenArea.x(); const QRect screenArea = workspace()->clientArea(ScreenArea, geometryRestore().center(), desktop()); int topMax = screenArea.y(); int rightMax = screenArea.x() + screenArea.width(); int bottomMax = screenArea.y() + screenArea.height(); int leftMax = screenArea.x(); QRect newGeom = geometryRestore(); // geometry(); QRect newClientGeom = newGeom.adjusted(border[Left], border[Top], -border[Right], -border[Bottom]); const QRect newGeomTall = QRect(newGeom.x(), screenArea.y(), newGeom.width(), screenArea.height()); // Full screen height const QRect newGeomWide = QRect(screenArea.x(), newGeom.y(), screenArea.width(), newGeom.height()); // Full screen width // Get the max strut point for each side where the window is (E.g. Highest point for // the bottom struts bounded by the window's left and right sides). // These 4 compute old bounds ... auto moveAreaFunc = workspace()->inUpdateClientArea() ? &Workspace::previousRestrictedMoveArea : //... the restricted areas changed &Workspace::restrictedMoveArea; //... when e.g. active desktop or screen changes for (const QRect &r : (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaTop)) { QRect rect = r & oldGeomTall; if (!rect.isEmpty()) oldTopMax = qMax(oldTopMax, rect.y() + rect.height()); } for (const QRect &r : (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaRight)) { QRect rect = r & oldGeomWide; if (!rect.isEmpty()) oldRightMax = qMin(oldRightMax, rect.x()); } for (const QRect &r : (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaBottom)) { QRect rect = r & oldGeomTall; if (!rect.isEmpty()) oldBottomMax = qMin(oldBottomMax, rect.y()); } for (const QRect &r : (workspace()->*moveAreaFunc)(oldDesktop, StrutAreaLeft)) { QRect rect = r & oldGeomWide; if (!rect.isEmpty()) oldLeftMax = qMax(oldLeftMax, rect.x() + rect.width()); } // These 4 compute new bounds for (const QRect &r : workspace()->restrictedMoveArea(desktop(), StrutAreaTop)) { QRect rect = r & newGeomTall; if (!rect.isEmpty()) topMax = qMax(topMax, rect.y() + rect.height()); } for (const QRect &r : workspace()->restrictedMoveArea(desktop(), StrutAreaRight)) { QRect rect = r & newGeomWide; if (!rect.isEmpty()) rightMax = qMin(rightMax, rect.x()); } for (const QRect &r : workspace()->restrictedMoveArea(desktop(), StrutAreaBottom)) { QRect rect = r & newGeomTall; if (!rect.isEmpty()) bottomMax = qMin(bottomMax, rect.y()); } for (const QRect &r : workspace()->restrictedMoveArea(desktop(), StrutAreaLeft)) { QRect rect = r & newGeomWide; if (!rect.isEmpty()) leftMax = qMax(leftMax, rect.x() + rect.width()); } // Check if the sides were inside or touching but are no longer bool keep[4] = {false, false, false, false}; bool save[4] = {false, false, false, false}; int padding[4] = {0, 0, 0, 0}; if (oldGeometry.x() >= oldLeftMax) save[Left] = newGeom.x() < leftMax; if (oldGeometry.x() == oldLeftMax) keep[Left] = newGeom.x() != leftMax; else if (oldClientGeometry.x() == oldLeftMax && newClientGeom.x() != leftMax) { padding[0] = border[Left]; keep[Left] = true; } if (oldGeometry.y() >= oldTopMax) save[Top] = newGeom.y() < topMax; if (oldGeometry.y() == oldTopMax) keep[Top] = newGeom.y() != topMax; else if (oldClientGeometry.y() == oldTopMax && newClientGeom.y() != topMax) { padding[1] = border[Left]; keep[Top] = true; } if (oldGeometry.right() <= oldRightMax - 1) save[Right] = newGeom.right() > rightMax - 1; if (oldGeometry.right() == oldRightMax - 1) keep[Right] = newGeom.right() != rightMax - 1; else if (oldClientGeometry.right() == oldRightMax - 1 && newClientGeom.right() != rightMax - 1) { padding[2] = border[Right]; keep[Right] = true; } if (oldGeometry.bottom() <= oldBottomMax - 1) save[Bottom] = newGeom.bottom() > bottomMax - 1; if (oldGeometry.bottom() == oldBottomMax - 1) keep[Bottom] = newGeom.bottom() != bottomMax - 1; else if (oldClientGeometry.bottom() == oldBottomMax - 1 && newClientGeom.bottom() != bottomMax - 1) { padding[3] = border[Bottom]; keep[Bottom] = true; } // if randomly touches opposing edges, do not favor either if (keep[Left] && keep[Right]) { keep[Left] = keep[Right] = false; padding[0] = padding[2] = 0; } if (keep[Top] && keep[Bottom]) { keep[Top] = keep[Bottom] = false; padding[1] = padding[3] = 0; } if (save[Left] || keep[Left]) newGeom.moveLeft(qMax(leftMax, screenArea.x()) - padding[0]); if (padding[0] && screens()->intersecting(newGeom) > 1) newGeom.moveLeft(newGeom.left() + padding[0]); if (save[Top] || keep[Top]) newGeom.moveTop(qMax(topMax, screenArea.y()) - padding[1]); if (padding[1] && screens()->intersecting(newGeom) > 1) newGeom.moveTop(newGeom.top() + padding[1]); if (save[Right] || keep[Right]) newGeom.moveRight(qMin(rightMax - 1, screenArea.right()) + padding[2]); if (padding[2] && screens()->intersecting(newGeom) > 1) newGeom.moveRight(newGeom.right() - padding[2]); if (oldGeometry.x() >= oldLeftMax && newGeom.x() < leftMax) newGeom.setLeft(qMax(leftMax, screenArea.x())); else if (oldClientGeometry.x() >= oldLeftMax && newGeom.x() + border[Left] < leftMax) { newGeom.setLeft(qMax(leftMax, screenArea.x()) - border[Left]); if (screens()->intersecting(newGeom) > 1) newGeom.setLeft(newGeom.left() + border[Left]); } if (save[Bottom] || keep[Bottom]) newGeom.moveBottom(qMin(bottomMax - 1, screenArea.bottom()) + padding[3]); if (padding[3] && screens()->intersecting(newGeom) > 1) newGeom.moveBottom(newGeom.bottom() - padding[3]); if (oldGeometry.y() >= oldTopMax && newGeom.y() < topMax) newGeom.setTop(qMax(topMax, screenArea.y())); else if (oldClientGeometry.y() >= oldTopMax && newGeom.y() + border[Top] < topMax) { newGeom.setTop(qMax(topMax, screenArea.y()) - border[Top]); if (screens()->intersecting(newGeom) > 1) newGeom.setTop(newGeom.top() + border[Top]); } checkOffscreenPosition(&newGeom, screenArea); // Obey size hints. TODO: We really should make sure it stays in the right place if (!isShade()) newGeom.setSize(constrainFrameSize(newGeom.size())); if (newGeom != frameGeometry()) setFrameGeometry(newGeom); } void AbstractClient::checkOffscreenPosition(QRect* geom, const QRect& screenArea) { if (geom->left() > screenArea.right()) { geom->moveLeft(screenArea.right() - screenArea.width()/4); } else if (geom->right() < screenArea.left()) { geom->moveRight(screenArea.left() + screenArea.width()/4); } if (geom->top() > screenArea.bottom()) { geom->moveTop(screenArea.bottom() - screenArea.height()/4); } else if (geom->bottom() < screenArea.top()) { geom->moveBottom(screenArea.top() + screenArea.width()/4); } } /** * Returns the appropriate frame size for the current client size. * * This is equivalent to clientSizeToFrameSize(constrainClientSize(clientSize())). */ QSize AbstractClient::adjustedSize() const { return clientSizeToFrameSize(constrainClientSize(clientSize())); } /** * Constrains the client size @p size according to a set of the window's size hints. * * Default implementation applies only minimum and maximum size constraints. */ QSize AbstractClient::constrainClientSize(const QSize &size, SizeMode mode) const { Q_UNUSED(mode) int width = size.width(); int height = size.height(); // When user is resizing the window, the move resize geometry may have negative width or // height. In which case, we need to set negative dimensions to reasonable values. if (width < 1) { width = 1; } if (height < 1) { height = 1; } const QSize minimumSize = minSize(); const QSize maximumSize = maxSize(); width = qBound(minimumSize.width(), width, maximumSize.width()); height = qBound(minimumSize.height(), height, maximumSize.height()); return QSize(width, height); } /** * Constrains the frame size @p size according to a set of the window's size hints. */ QSize AbstractClient::constrainFrameSize(const QSize &size, SizeMode mode) const { const QSize unconstrainedClientSize = frameSizeToClientSize(size); const QSize constrainedClientSize = constrainClientSize(unconstrainedClientSize, mode); return clientSizeToFrameSize(constrainedClientSize); } /** * Returns @c true if the AbstractClient can be shown in full screen mode; otherwise @c false. * * Default implementation returns @c false. */ bool AbstractClient::isFullScreenable() const { return false; } /** * Returns @c true if the AbstractClient is currently being shown in full screen mode; otherwise @c false. * * A client in full screen mode occupies the entire screen with no window frame around it. * * Default implementation returns @c false. */ bool AbstractClient::isFullScreen() const { return false; } /** * Returns whether requests initiated by the user to enter or leave full screen mode are honored. * * Default implementation returns @c false. */ bool AbstractClient::userCanSetFullScreen() const { return false; } /** * Asks the AbstractClient to enter or leave full screen mode. * * Default implementation does nothing. * * @param set @c true if the AbstractClient has to be shown in full screen mode, otherwise @c false * @param user @c true if the request is initiated by the user, otherwise @c false */ void AbstractClient::setFullScreen(bool set, bool user) { Q_UNUSED(set) Q_UNUSED(user) } /** * Returns @c true if the AbstractClient can be minimized; otherwise @c false. * * Default implementation returns @c false. */ bool AbstractClient::isMinimizable() const { return false; } /** * Returns @c true if the AbstractClient can be maximized; otherwise @c false. * * Default implementation returns @c false. */ bool AbstractClient::isMaximizable() const { return false; } /** * Returns the currently applied maximize mode. * * Default implementation returns MaximizeRestore. */ MaximizeMode AbstractClient::maximizeMode() const { return MaximizeRestore; } /** * Returns the last requested maximize mode. * * On X11, this method always matches maximizeMode(). On Wayland, it is asynchronous. * * Default implementation matches maximizeMode(). */ MaximizeMode AbstractClient::requestedMaximizeMode() const { return maximizeMode(); } /** * Returns the geometry of the AbstractClient before it was maximized or quick tiled. */ QRect AbstractClient::geometryRestore() const { return m_maximizeGeometryRestore; } /** * Sets the geometry of the AbstractClient before it was maximized or quick tiled to @p rect. */ void AbstractClient::setGeometryRestore(const QRect &rect) { m_maximizeGeometryRestore = rect; } /** * Toggles the maximized state along specified dimensions @p horizontal and @p vertical. * * If @p adjust is @c true, only frame geometry will be updated to match requestedMaximizeMode(). * * Default implementation does nothing. */ void AbstractClient::changeMaximize(bool horizontal, bool vertical, bool adjust) { Q_UNUSED(horizontal) Q_UNUSED(vertical) Q_UNUSED(adjust) } } diff --git a/abstract_client.h b/abstract_client.h index 6e619ca61..e574d245e 100644 --- a/abstract_client.h +++ b/abstract_client.h @@ -1,1369 +1,1370 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 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 "cursor.h" #include #include #include namespace KWaylandServer { class PlasmaWindowInterface; } namespace KDecoration2 { class Decoration; } namespace KWin { class Group; namespace TabBox { class TabBoxClientImpl; } namespace Decoration { class DecoratedClientImpl; class DecorationPalette; } class KWIN_EXPORT AbstractClient : public Toplevel { Q_OBJECT /** * Whether this Client is fullScreen. A Client might either be fullScreen due to the _NET_WM property * or through a legacy support hack. The fullScreen state can only be changed if the Client does not * use the legacy hack. To be sure whether the state changed, connect to the notify signal. */ Q_PROPERTY(bool fullScreen READ isFullScreen WRITE setFullScreen NOTIFY fullScreenChanged) /** * Whether the Client can be set to fullScreen. The property is evaluated each time it is invoked. * Because of that there is no notify signal. */ Q_PROPERTY(bool fullScreenable READ isFullScreenable) /** * Whether this Client is active or not. Use Workspace::activateClient() to activate a Client. * @see Workspace::activateClient */ Q_PROPERTY(bool active READ isActive NOTIFY activeChanged) /** * The desktop this Client is on. If the Client is on all desktops the property has value -1. * This is a legacy property, use x11DesktopIds instead */ Q_PROPERTY(int desktop READ desktop WRITE setDesktop NOTIFY desktopChanged) /** * Whether the Client is on all desktops. That is desktop is -1. */ Q_PROPERTY(bool onAllDesktops READ isOnAllDesktops WRITE setOnAllDesktops NOTIFY desktopChanged) /** * The x11 ids for all desktops this client is in. On X11 this list will always have a length of 1 */ Q_PROPERTY(QVector x11DesktopIds READ x11DesktopIds NOTIFY x11DesktopIdsChanged) /** * Indicates that the window should not be included on a taskbar. */ Q_PROPERTY(bool skipTaskbar READ skipTaskbar WRITE setSkipTaskbar NOTIFY skipTaskbarChanged) /** * Indicates that the window should not be included on a Pager. */ Q_PROPERTY(bool skipPager READ skipPager WRITE setSkipPager NOTIFY skipPagerChanged) /** * Whether the Client should be excluded from window switching effects. */ Q_PROPERTY(bool skipSwitcher READ skipSwitcher WRITE setSkipSwitcher NOTIFY skipSwitcherChanged) /** * Whether the window can be closed by the user. The value is evaluated each time the getter is called. * Because of that no changed signal is provided. */ Q_PROPERTY(bool closeable READ isCloseable) Q_PROPERTY(QIcon icon READ icon NOTIFY iconChanged) /** * Whether the Client is set to be kept above other windows. */ Q_PROPERTY(bool keepAbove READ keepAbove WRITE setKeepAbove NOTIFY keepAboveChanged) /** * Whether the Client is set to be kept below other windows. */ Q_PROPERTY(bool keepBelow READ keepBelow WRITE setKeepBelow NOTIFY keepBelowChanged) /** * Whether the Client can be shaded. The property is evaluated each time it is invoked. * Because of that there is no notify signal. */ Q_PROPERTY(bool shadeable READ isShadeable) /** * Whether the Client is shaded. */ Q_PROPERTY(bool shade READ isShade WRITE setShade NOTIFY shadeChanged) /** * Whether the Client can be minimized. The property is evaluated each time it is invoked. * Because of that there is no notify signal. */ Q_PROPERTY(bool minimizable READ isMinimizable) /** * Whether the Client is minimized. */ Q_PROPERTY(bool minimized READ isMinimized WRITE setMinimized NOTIFY minimizedChanged) /** * The optional geometry representing the minimized Client in e.g a taskbar. * See _NET_WM_ICON_GEOMETRY at https://standards.freedesktop.org/wm-spec/wm-spec-latest.html . * The value is evaluated each time the getter is called. * Because of that no changed signal is provided. */ Q_PROPERTY(QRect iconGeometry READ iconGeometry) /** * Returns whether the window is any of special windows types (desktop, dock, splash, ...), * i.e. window types that usually don't have a window frame and the user does not use window * management (moving, raising,...) on them. * The value is evaluated each time the getter is called. * Because of that no changed signal is provided. */ Q_PROPERTY(bool specialWindow READ isSpecialWindow) /** * Whether window state _NET_WM_STATE_DEMANDS_ATTENTION is set. This state indicates that some * action in or with the window happened. For example, it may be set by the Window Manager if * the window requested activation but the Window Manager refused it, or the application may set * it if it finished some work. This state may be set by both the Client and the Window Manager. * It should be unset by the Window Manager when it decides the window got the required attention * (usually, that it got activated). */ Q_PROPERTY(bool demandsAttention READ isDemandingAttention WRITE demandAttention NOTIFY demandsAttentionChanged) /** * The Caption of the Client. Read from WM_NAME property together with a suffix for hostname and shortcut. * To read only the caption as provided by WM_NAME, use the getter with an additional @c false value. */ Q_PROPERTY(QString caption READ caption NOTIFY captionChanged) /** * Minimum size as specified in WM_NORMAL_HINTS */ Q_PROPERTY(QSize minSize READ minSize) /** * Maximum size as specified in WM_NORMAL_HINTS */ Q_PROPERTY(QSize maxSize READ maxSize) /** * Whether the Client can accept keyboard focus. * The value is evaluated each time the getter is called. * Because of that no changed signal is provided. */ Q_PROPERTY(bool wantsInput READ wantsInput) /** * Whether the Client is a transient Window to another Window. * @see transientFor */ Q_PROPERTY(bool transient READ isTransient NOTIFY transientChanged) /** * The Client to which this Client is a transient if any. */ Q_PROPERTY(KWin::AbstractClient *transientFor READ transientFor NOTIFY transientChanged) /** * Whether the Client represents a modal window. */ Q_PROPERTY(bool modal READ isModal NOTIFY modalChanged) /** * The geometry of this Client. Be aware that depending on resize mode the frameGeometryChanged * signal might be emitted at each resize step or only at the end of the resize operation. * * @deprecated Use frameGeometry */ Q_PROPERTY(QRect geometry READ frameGeometry WRITE setFrameGeometry) /** * The geometry of this Client. Be aware that depending on resize mode the frameGeometryChanged * signal might be emitted at each resize step or only at the end of the resize operation. */ Q_PROPERTY(QRect frameGeometry READ frameGeometry WRITE setFrameGeometry) /** * Whether the Client is currently being moved by the user. * Notify signal is emitted when the Client starts or ends move/resize mode. */ Q_PROPERTY(bool move READ isMove NOTIFY moveResizedChanged) /** * Whether the Client is currently being resized by the user. * Notify signal is emitted when the Client starts or ends move/resize mode. */ Q_PROPERTY(bool resize READ isResize NOTIFY moveResizedChanged) /** * Whether the decoration is currently using an alpha channel. */ Q_PROPERTY(bool decorationHasAlpha READ decorationHasAlpha) /** * Whether the window has a decoration or not. * This property is not allowed to be set by applications themselves. * The decision whether a window has a border or not belongs to the window manager. * If this property gets abused by application developers, it will be removed again. */ Q_PROPERTY(bool noBorder READ noBorder WRITE setNoBorder) /** * Whether the Client provides context help. Mostly needed by decorations to decide whether to * show the help button or not. */ Q_PROPERTY(bool providesContextHelp READ providesContextHelp CONSTANT) /** * Whether the Client can be maximized both horizontally and vertically. * The property is evaluated each time it is invoked. * Because of that there is no notify signal. */ Q_PROPERTY(bool maximizable READ isMaximizable) /** * Whether the Client is moveable. Even if it is not moveable, it might be possible to move * it to another screen. The property is evaluated each time it is invoked. * Because of that there is no notify signal. * @see moveableAcrossScreens */ Q_PROPERTY(bool moveable READ isMovable) /** * Whether the Client can be moved to another screen. The property is evaluated each time it is invoked. * Because of that there is no notify signal. * @see moveable */ Q_PROPERTY(bool moveableAcrossScreens READ isMovableAcrossScreens) /** * Whether the Client can be resized. The property is evaluated each time it is invoked. * Because of that there is no notify signal. */ Q_PROPERTY(bool resizeable READ isResizable) /** * The desktop file name of the application this AbstractClient belongs to. * * This is either the base name without full path and without file extension of the * desktop file for the window's application (e.g. "org.kde.foo"). * * The application's desktop file name can also be the full path to the desktop file * (e.g. "/opt/kde/share/org.kde.foo.desktop") in case it's not in a standard location. */ Q_PROPERTY(QByteArray desktopFileName READ desktopFileName NOTIFY desktopFileNameChanged) /** * Whether an application menu is available for this Client */ Q_PROPERTY(bool hasApplicationMenu READ hasApplicationMenu NOTIFY hasApplicationMenuChanged) /** * Whether the application menu for this Client is currently opened */ Q_PROPERTY(bool applicationMenuActive READ applicationMenuActive NOTIFY applicationMenuActiveChanged) /** * Whether this client is unresponsive. * * When an application failed to react on a ping request in time, it is * considered unresponsive. This usually indicates that the application froze or crashed. */ Q_PROPERTY(bool unresponsive READ unresponsive NOTIFY unresponsiveChanged) /** * The color scheme set on this client * Absolute file path, or name of palette in the user's config directory following KColorSchemes format. * An empty string indicates the default palette from kdeglobals is used. * @note this indicates the colour scheme requested, which might differ from the theme applied if the colorScheme cannot be found */ Q_PROPERTY(QString colorScheme READ colorScheme NOTIFY colorSchemeChanged) public: ~AbstractClient() override; QWeakPointer tabBoxClient() const { return m_tabBoxClient.toWeakRef(); } bool isFirstInTabBox() const { return m_firstInTabBox; } bool skipSwitcher() const { return m_skipSwitcher; } void setSkipSwitcher(bool set); bool skipTaskbar() const { return m_skipTaskbar; } void setSkipTaskbar(bool set); void setOriginalSkipTaskbar(bool set); bool originalSkipTaskbar() const { return m_originalSkipTaskbar; } bool skipPager() const { return m_skipPager; } void setSkipPager(bool set); const QIcon &icon() const { return m_icon; } bool isActive() const { return m_active; } /** * Sets the client's active state to \a act. * * This function does only change the visual appearance of the client, * it does not change the focus setting. Use * Workspace::activateClient() or Workspace::requestFocus() instead. * * If a client receives or looses the focus, it calls setActive() on * its own. */ void setActive(bool); bool keepAbove() const { return m_keepAbove; } void setKeepAbove(bool); bool keepBelow() const { return m_keepBelow; } void setKeepBelow(bool); void demandAttention(bool set = true); bool isDemandingAttention() const { return m_demandsAttention; } void cancelAutoRaise(); bool wantsTabFocus() const; QMargins frameMargins() const override; QPoint clientPos() const override { return QPoint(borderLeft(), borderTop()); } virtual void updateMouseGrab(); /** * @returns The caption consisting of captionNormal and captionSuffix * @see captionNormal * @see captionSuffix */ QString caption() const; /** * @returns The caption as set by the AbstractClient without any suffix. * @see caption * @see captionSuffix */ virtual QString captionNormal() const = 0; /** * @returns The suffix added to the caption (e.g. shortcut, machine name, etc.) * @see caption * @see captionNormal */ virtual QString captionSuffix() const = 0; virtual bool isCloseable() const = 0; // TODO: remove boolean trap virtual bool isShown(bool shaded_is_shown) const = 0; virtual bool isHiddenInternal() const = 0; // TODO: remove boolean trap virtual void hideClient(bool hide) = 0; virtual bool isFullScreenable() const; virtual bool isFullScreen() const; // TODO: remove boolean trap virtual AbstractClient *findModal(bool allow_itself = false) = 0; virtual bool isTransient() const; /** * @returns Whether there is a hint available to place the AbstractClient on it's parent, default @c false. * @see transientPlacementHint */ virtual bool hasTransientPlacementHint() const; /** * Only valid id hasTransientPlacementHint is true * @returns The position the transient wishes to position itself */ virtual QRect transientPlacement(const QRect &bounds) const; const AbstractClient* transientFor() const; AbstractClient* transientFor(); /** * @returns @c true if c is the transient_for window for this client, * or recursively the transient_for window * @todo: remove boolean trap */ virtual bool hasTransient(const AbstractClient* c, bool indirect) const; const QList& transients() const; // Is not indirect virtual void addTransient(AbstractClient *client); virtual void removeTransient(AbstractClient* cl); virtual QList mainClients() const; // Call once before loop , is not indirect QList allMainClients() const; // Call once before loop , is indirect /** * Returns true for "special" windows and false for windows which are "normal" * (normal=window which has a border, can be moved by the user, can be closed, etc.) * true for Desktop, Dock, Splash, Override and TopMenu (and Toolbar??? - for now) * false for Normal, Dialog, Utility and Menu (and Toolbar??? - not yet) TODO */ bool isSpecialWindow() const; void sendToScreen(int screen); const QKeySequence &shortcut() const { return _shortcut; } void setShortcut(const QString &cut); bool performMouseCommand(Options::MouseCommand, const QPoint &globalPos); void setOnAllDesktops(bool set); void setDesktop(int); void enterDesktop(VirtualDesktop *desktop); void leaveDesktop(VirtualDesktop *desktop); /** * Set the window as being on the attached list of desktops * On X11 it will be set to the last entry */ void setDesktops(QVector desktops); int desktop() const override { return m_desktops.isEmpty() ? (int)NET::OnAllDesktops : m_desktops.last()->x11DesktopNumber(); } QVector desktops() const override { return m_desktops; } QVector x11DesktopIds() const; void setMinimized(bool set); /** * Minimizes this client plus its transients */ void minimize(bool avoid_animation = false); void unminimize(bool avoid_animation = false); bool isMinimized() const { return m_minimized; } virtual void setFullScreen(bool set, bool user = true); virtual void setClientShown(bool shown); QRect geometryRestore() const; virtual MaximizeMode maximizeMode() const; virtual MaximizeMode requestedMaximizeMode() const; void maximize(MaximizeMode); /** * Sets the maximization according to @p vertically and @p horizontally. */ Q_INVOKABLE void setMaximize(bool vertically, bool horizontally); virtual bool noBorder() const = 0; virtual void setNoBorder(bool set) = 0; virtual void blockActivityUpdates(bool b = true) = 0; QPalette palette() const; const Decoration::DecorationPalette *decorationPalette() const; /** * Returns whether the window is resizable or has a fixed size. */ virtual bool isResizable() const = 0; /** * Returns whether the window is moveable or has a fixed position. */ virtual bool isMovable() const = 0; /** * Returns whether the window can be moved to another screen. */ virtual bool isMovableAcrossScreens() const = 0; /** * @c true only for @c ShadeNormal */ bool isShade() const { return shadeMode() == ShadeNormal; } ShadeMode shadeMode() const; // Prefer isShade() void setShade(bool set); void setShade(ShadeMode mode); void toggleShade(); void cancelShadeHoverTimer(); /** * Whether the Client can be shaded. Default implementation returns @c false. */ virtual bool isShadeable() const; virtual bool isMaximizable() const; virtual bool isMinimizable() const; virtual QRect iconGeometry() const; virtual bool userCanSetFullScreen() const; virtual bool userCanSetNoBorder() const = 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(); virtual 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); /** * Sets the quick tile mode ("snap") of this window. * This will also handle preserving and restoring of window geometry as necessary. * @param mode The tile mode (left/right) to give this window. * @param keyboard Defines whether to take keyboard cursor into account. */ void setQuickTileMode(QuickTileMode mode, bool keyboard = false); QuickTileMode quickTileMode() const { return QuickTileMode(m_quickTileMode); } Layer layer() const override; void updateLayer(); void placeIn(const QRect &area); enum ForceGeometry_t { NormalGeometrySet, ForceGeometrySet }; virtual void move(int x, int y, ForceGeometry_t force = NormalGeometrySet); void move(const QPoint &p, ForceGeometry_t force = NormalGeometrySet); virtual void resizeWithChecks(const QSize& s, ForceGeometry_t force = NormalGeometrySet) = 0; void keepInArea(QRect area, bool partial = false); virtual QSize minSize() const; virtual QSize maxSize() const; virtual void setFrameGeometry(const QRect &rect, ForceGeometry_t force = NormalGeometrySet) = 0; /** * How to resize the window in order to obey constraints (mainly aspect ratios). */ enum SizeMode { SizeModeAny, SizeModeFixedW, ///< Try not to affect width SizeModeFixedH, ///< Try not to affect height SizeModeMax ///< Try not to make it larger in either direction }; virtual QSize constrainClientSize(const QSize &size, SizeMode mode = SizeModeAny) const; QSize constrainFrameSize(const QSize &size, SizeMode mode = SizeModeAny) const; QSize adjustedSize() const; /** * Calculates the matching client position for the given frame position @p point. */ virtual QPoint framePosToClientPos(const QPoint &point) const; /** * Calculates the matching frame position for the given client position @p point. */ virtual QPoint clientPosToFramePos(const QPoint &point) const; /** * Calculates the matching client size for the given frame size @p size. * * Notice that size constraints won't be applied. * * Default implementation returns the frame size with frame margins being excluded. */ virtual QSize frameSizeToClientSize(const QSize &size) const; /** * Calculates the matching frame size for the given client size @p size. * * Notice that size constraints won't be applied. * * Default implementation returns the client size with frame margins being included. */ virtual QSize clientSizeToFrameSize(const QSize &size) const; /** * Calculates the matching client rect for the given frame rect @p rect. * * Notice that size constraints won't be applied. */ QRect frameRectToClientRect(const QRect &rect) const; /** * Calculates the matching frame rect for the given client rect @p rect. * * Notice that size constraints won't be applied. */ QRect clientRectToFrameRect(const QRect &rect) const; /** * Returns @c true if the Client is being interactively moved; otherwise @c false. */ bool isMove() const { return isMoveResize() && moveResizePointerMode() == PositionCenter; } /** * Returns @c true if the Client is being interactively resized; otherwise @c false. */ bool isResize() const { return isMoveResize() && moveResizePointerMode() != PositionCenter; } /** * Cursor shape for move/resize mode. */ CursorShape cursor() const { return m_moveResize.cursor; } virtual 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; /** * @returns the geometry of the virtual keyboard * This geometry is in global coordinates */ QRect virtualKeyboardGeometry() const; /** * Sets the geometry of the virtual keyboard, The window may resize itself in order to make space for the keybaord * This geometry is in global coordinates */ void setVirtualKeyboardGeometry(const QRect &geo); /** * Restores the AbstractClient after it had been hidden due to show on screen edge functionality. * The AbstractClient also gets raised (e.g. Panel mode windows can cover) and the AbstractClient * gets informed in a window specific way that it is shown and raised again. */ virtual void showOnScreenEdge() = 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; virtual void destroyClient() = 0; enum class SameApplicationCheck { RelaxedForActive = 1 << 0, AllowCrossProcesses = 1 << 1 }; Q_DECLARE_FLAGS(SameApplicationChecks, SameApplicationCheck) static bool belongToSameApplication(const AbstractClient* c1, const AbstractClient* c2, SameApplicationChecks checks = SameApplicationChecks()); bool hasApplicationMenu() const; bool applicationMenuActive() const { return m_applicationMenuActive; } void setApplicationMenuActive(bool applicationMenuActive); QString applicationMenuServiceName() const { return m_applicationMenuServiceName; } QString applicationMenuObjectPath() const { return m_applicationMenuObjectPath; } QString colorScheme() const { return m_colorScheme; } /** * Request showing the application menu bar * @param actionId The DBus menu ID of the action that should be highlighted, 0 for the root menu */ void showApplicationMenu(int actionId); bool unresponsive() const; virtual bool isInitialPositionSet() const { return false; } /** * Default implementation returns @c null. * Mostly intended for X11 clients, from EWMH: * @verbatim * If the WM_TRANSIENT_FOR property is set to None or Root window, the window should be * treated as a transient for all other windows in the same group. It has been noted that this * is a slight ICCCM violation, but as this behavior is pretty standard for many toolkits and * window managers, and is extremely unlikely to break anything, it seems reasonable to document * it as standard. * @endverbatim */ virtual bool groupTransient() const; /** * Default implementation returns @c null. * * Mostly for X11 clients, holds the client group */ virtual const Group *group() const; /** * Default implementation returns @c null. * * Mostly for X11 clients, holds the client group */ virtual Group *group(); /** * Returns whether this is an internal client. * * Internal clients are created by KWin and used for special purpose windows, * like the task switcher, etc. * * Default implementation returns @c false. */ virtual bool isInternal() const; /** * Returns whether window rules can be applied to this client. * * Default implementation returns @c true. */ virtual bool supportsWindowRules() const; /** * Return window management interface */ KWaylandServer::PlasmaWindowInterface *windowManagementInterface() const { return m_windowManagementInterface; } public Q_SLOTS: virtual void closeWindow() = 0; Q_SIGNALS: void fullScreenChanged(); void skipTaskbarChanged(); void skipPagerChanged(); void skipSwitcherChanged(); void iconChanged(); void activeChanged(); void keepAboveChanged(bool); void keepBelowChanged(bool); /** * Emitted whenever the demands attention state changes. */ void demandsAttentionChanged(); void desktopPresenceChanged(KWin::AbstractClient*, int); // to be forwarded by Workspace void desktopChanged(); void x11DesktopIdsChanged(); void shadeChanged(); void minimizedChanged(); void clientMinimized(KWin::AbstractClient* client, bool animate); void clientUnminimized(KWin::AbstractClient* client, bool animate); void paletteChanged(const QPalette &p); void colorSchemeChanged(); void captionChanged(); void clientMaximizedStateChanged(KWin::AbstractClient*, MaximizeMode); void clientMaximizedStateChanged(KWin::AbstractClient* c, bool h, bool v); void transientChanged(); void modalChanged(); void quickTileModeChanged(); void moveResizedChanged(); void moveResizeCursorChanged(CursorShape); void clientStartUserMovedResized(KWin::AbstractClient*); void clientStepUserMovedResized(KWin::AbstractClient *, const QRect&); void clientFinishUserMovedResized(KWin::AbstractClient*); void closeableChanged(bool); void minimizeableChanged(bool); void shadeableChanged(bool); void maximizeableChanged(bool); void desktopFileNameChanged(); void applicationMenuChanged(); void hasApplicationMenuChanged(bool); void applicationMenuActiveChanged(bool); void unresponsiveChanged(bool); protected: AbstractClient(); void setFirstInTabBox(bool enable) { m_firstInTabBox = enable; } void setIcon(const QIcon &icon); void startAutoRaise(); void autoRaise(); /** * Whether the window accepts focus. * The difference to wantsInput is that the implementation should not check rules and return * what the window effectively supports. */ virtual bool acceptsFocus() const = 0; /** * Called from setActive once the active value got updated, but before the changed signal * is emitted. * * Default implementation does nothing. */ virtual void doSetActive(); /** * Called from setKeepAbove once the keepBelow value got updated, but before the changed signal * is emitted. * * Default implementation does nothing. */ virtual void doSetKeepAbove(); /** * Called from setKeepBelow once the keepBelow value got updated, but before the changed signal * is emitted. * * Default implementation does nothing. */ virtual void doSetKeepBelow(); /** * Called from setShade() once the shadeMode value got updated, but before the changed signal * is emitted. * * Default implementation does nothing. */ virtual void doSetShade(ShadeMode previousShadeMode); /** * Called from setDeskop once the desktop value got updated, but before the changed signal * is emitted. * * Default implementation does nothing. */ virtual void doSetDesktop(); /** * Called from @ref minimize and @ref unminimize once the minimized value got updated, but before the * changed signal is emitted. * * Default implementation does nothig. */ virtual void doMinimize(); virtual bool belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const = 0; virtual void doSetSkipTaskbar(); virtual void doSetSkipPager(); virtual void doSetSkipSwitcher(); virtual void doSetDemandsAttention(); void setupWindowManagementInterface(); void destroyWindowManagementInterface(); void updateColorScheme(QString path); virtual void updateColorScheme() = 0; void setTransientFor(AbstractClient *transientFor); /** * 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; } // geometry handling void checkOffscreenPosition(QRect *geom, const QRect &screenArea); int borderLeft() const; int borderRight() const; int borderTop() const; int borderBottom() const; virtual void changeMaximize(bool horizontal, bool vertical, bool adjust); void setGeometryRestore(const QRect &rect); /** * Called from move after updating the geometry. Can be reimplemented to perform specific tasks. * The base implementation does nothing. */ virtual void doMove(int x, int y); void blockGeometryUpdates(bool block); void blockGeometryUpdates(); void unblockGeometryUpdates(); bool areGeometryUpdatesBlocked() const; enum PendingGeometry_t { PendingGeometryNone, PendingGeometryNormal, PendingGeometryForced }; PendingGeometry_t pendingGeometryUpdate() const; void setPendingGeometryUpdate(PendingGeometry_t update); QRect bufferGeometryBeforeUpdateBlocking() const; QRect frameGeometryBeforeUpdateBlocking() const; void updateGeometryBeforeUpdateBlocking(); /** * Schedules a repaint for the visibleRect before and after a * geometry update. The current visibleRect is stored for the * next time this method is called as the before geometry. */ void addRepaintDuringGeometryUpdates(); /** * @returns whether the Client is currently in move resize mode */ bool isMoveResize() const { return m_moveResize.enabled; } /** * Sets whether the Client is in move resize mode to @p enabled. */ void setMoveResize(bool enabled) { m_moveResize.enabled = enabled; } /** * @returns whether the move resize mode is unrestricted. */ bool isUnrestrictedMoveResize() const { return m_moveResize.unrestricted; } /** * Sets whether move resize mode is unrestricted to @p set. */ void setUnrestrictedMoveResize(bool set) { m_moveResize.unrestricted = set; } QPoint moveOffset() const { return m_moveResize.offset; } void setMoveOffset(const QPoint &offset) { m_moveResize.offset = offset; } QPoint invertedMoveOffset() const { return m_moveResize.invertedOffset; } void setInvertedMoveOffset(const QPoint &offset) { m_moveResize.invertedOffset = offset; } QRect initialMoveResizeGeometry() const { return m_moveResize.initialGeometry; } /** * Sets the initial move resize geometry to the current geometry. */ void updateInitialMoveResizeGeometry(); QRect moveResizeGeometry() const { return m_moveResize.geometry; } void setMoveResizeGeometry(const QRect &geo) { m_moveResize.geometry = geo; } Position moveResizePointerMode() const { return m_moveResize.pointer; } void setMoveResizePointerMode(Position mode) { m_moveResize.pointer = mode; } bool isMoveResizePointerButtonDown() const { return m_moveResize.buttonDown; } void setMoveResizePointerButtonDown(bool down) { m_moveResize.buttonDown = down; } int moveResizeStartScreen() const { return m_moveResize.startScreen; } void checkUnrestrictedMoveResize(); /** * Sets an appropriate cursor shape for the logical mouse position. */ void updateCursor(); void startDelayedMoveResize(); void stopDelayedMoveResize(); bool startMoveResize(); /** * Called from startMoveResize. * * Implementing classes should return @c false if starting move resize should * get aborted. In that case startMoveResize will also return @c false. * * Base implementation returns @c true. */ virtual bool doStartMoveResize(); + virtual void doFinishMoveResize(); void finishMoveResize(bool cancel); /** * Leaves the move resize mode. * * Inheriting classes must invoke the base implementation which * ensures that the internal mode is properly ended. */ virtual void leaveMoveResize(); virtual void positionGeometryTip(); void performMoveResize(); /** * Called from performMoveResize() after actually performing the change of geometry. * Implementing subclasses can perform windowing system specific handling here. * * Default implementation does nothing. */ virtual void doPerformMoveResize(); /* * Checks if the mouse cursor is near the edge of the screen and if so * activates quick tiling or maximization */ void checkQuickTilingMaximizationZones(int xroot, int yroot); /** * Whether a sync request is still pending. * Default implementation returns @c false. */ virtual bool isWaitingForMoveResizeSync() const; /** * Called during handling a resize. Implementing subclasses can use this * method to perform windowing system specific syncing. * * Default implementation does nothing. */ virtual void doResizeSync(); void handleMoveResize(int x, int y, int x_root, int y_root); void handleMoveResize(const QPoint &local, const QPoint &global); void dontMoveResize(); virtual QSize resizeIncrements() const; /** * Returns the position depending on the Decoration's section under mouse. * If no decoration it returns PositionCenter. */ Position mousePosition() const; static bool haveResizeEffect() { return s_haveResizeEffect; } static void updateHaveResizeEffect(); static void resetHaveResizeEffect() { s_haveResizeEffect = false; } void setDecoration(KDecoration2::Decoration *decoration) { m_decoration.decoration = decoration; } virtual void createDecoration(const QRect &oldGeometry); virtual void destroyDecoration(); void startDecorationDoubleClickTimer(); void invalidateDecorationDoubleClickTimer(); void setDesktopFileName(QByteArray name); QString iconFromDesktopFile() const; void updateApplicationMenuServiceName(const QString &serviceName); void updateApplicationMenuObjectPath(const QString &objectPath); void setUnresponsive(bool unresponsive); virtual void setShortcutInternal(); QString shortcutCaptionSuffix() const; virtual void updateCaption() = 0; /** * Looks for another AbstractClient with same captionNormal and captionSuffix. * If no such AbstractClient exists @c nullptr is returned. */ AbstractClient *findClientWithSameCaption() const; void finishWindowRules(); void discardTemporaryRules(); bool tabTo(AbstractClient *other, bool behind, bool activate); void startShadeHoverTimer(); void startShadeUnhoverTimer(); private Q_SLOTS: void shadeHover(); void shadeUnhover(); private: void handlePaletteChange(); QSharedPointer m_tabBoxClient; bool m_firstInTabBox = false; bool m_skipTaskbar = false; /** * Unaffected by KWin */ bool m_originalSkipTaskbar = false; bool m_skipPager = false; bool m_skipSwitcher = false; QIcon m_icon; bool m_active = false; bool m_keepAbove = false; bool m_keepBelow = false; bool m_demandsAttention = false; bool m_minimized = false; QTimer *m_autoRaiseTimer = nullptr; QTimer *m_shadeHoverTimer = nullptr; ShadeMode m_shadeMode = ShadeNone; QVector m_desktops; QString m_colorScheme; std::shared_ptr m_palette; static QHash> s_palettes; static std::shared_ptr s_defaultPalette; KWaylandServer::PlasmaWindowInterface *m_windowManagementInterface = nullptr; AbstractClient *m_transientFor = nullptr; QList m_transients; bool m_modal = false; Layer m_layer = UnknownLayer; // electric border/quick tiling QuickTileMode m_electricMode = QuickTileFlag::None; bool m_electricMaximizing = false; // The quick tile mode of this window. int m_quickTileMode = int(QuickTileFlag::None); QTimer *m_electricMaximizingDelay = nullptr; // geometry int m_blockGeometryUpdates = 0; // > 0 = New geometry is remembered, but not actually set PendingGeometry_t m_pendingGeometryUpdate = PendingGeometryNone; friend class GeometryUpdatesBlocker; QRect m_visibleRectBeforeGeometryUpdate; QRect m_bufferGeometryBeforeUpdateBlocking; QRect m_frameGeometryBeforeUpdateBlocking; QRect m_virtualKeyboardGeometry; QRect m_keyboardGeometryRestore; QRect m_maximizeGeometryRestore; struct { bool enabled = false; bool unrestricted = false; QPoint offset; QPoint invertedOffset; QRect initialGeometry; QRect geometry; Position pointer = PositionCenter; bool buttonDown = false; CursorShape cursor = Qt::ArrowCursor; int startScreen = 0; QTimer *delayedTimer = nullptr; } m_moveResize; struct { KDecoration2::Decoration *decoration = nullptr; QPointer client; QElapsedTimer doubleClickTimer; } m_decoration; QByteArray m_desktopFileName; bool m_applicationMenuActive = false; QString m_applicationMenuServiceName; QString m_applicationMenuObjectPath; bool m_unresponsive = false; QKeySequence _shortcut; WindowRules m_rules; static bool s_haveResizeEffect; }; /** * Helper for AbstractClient::blockGeometryUpdates() being called in pairs (true/false) */ class GeometryUpdatesBlocker { public: explicit GeometryUpdatesBlocker(AbstractClient* c) : cl(c) { cl->blockGeometryUpdates(true); } ~GeometryUpdatesBlocker() { cl->blockGeometryUpdates(false); } private: AbstractClient* cl; }; inline void AbstractClient::move(const QPoint& p, ForceGeometry_t force) { move(p.x(), p.y(), force); } inline const QList& AbstractClient::transients() const { return m_transients; } inline bool AbstractClient::areGeometryUpdatesBlocked() const { return m_blockGeometryUpdates != 0; } inline void AbstractClient::blockGeometryUpdates() { m_blockGeometryUpdates++; } inline void AbstractClient::unblockGeometryUpdates() { m_blockGeometryUpdates--; } inline AbstractClient::PendingGeometry_t AbstractClient::pendingGeometryUpdate() const { return m_pendingGeometryUpdate; } inline void AbstractClient::setPendingGeometryUpdate(PendingGeometry_t update) { m_pendingGeometryUpdate = update; } } Q_DECLARE_METATYPE(KWin::AbstractClient*) Q_DECLARE_METATYPE(QList) Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::AbstractClient::SameApplicationChecks) #endif diff --git a/autotests/integration/maximize_test.cpp b/autotests/integration/maximize_test.cpp index e1611e774..72376267b 100644 --- a/autotests/integration/maximize_test.cpp +++ b/autotests/integration/maximize_test.cpp @@ -1,427 +1,432 @@ /******************************************************************** 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 "abstract_client.h" #include "cursor.h" #include "decorations/decorationbridge.h" #include "decorations/settings.h" #include "platform.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include #include #include #include #include #include #include #include #include #include using namespace KWin; using namespace KWayland::Client; static const QString s_socketName = QStringLiteral("wayland_test_kwin_maximized-0"); class TestMaximized : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testMaximizedPassedToDeco(); void testInitiallyMaximized(); void testInitiallyMaximizedBorderless(); void testBorderlessMaximizedWindow(); void testBorderlessMaximizedWindowNoClientSideDecoration(); }; void TestMaximized::initTestCase() { qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); kwinApp()->setConfig(KSharedConfig::openConfig(QString(), KConfig::SimpleConfig)); kwinApp()->start(); QVERIFY(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 | Test::AdditionalWaylandInterface::XdgDecoration | Test::AdditionalWaylandInterface::PlasmaShell)); screens()->setCurrent(0); KWin::Cursors::self()->mouse()->setPos(QPoint(1280, 512)); } void TestMaximized::cleanup() { Test::destroyWaylandConnection(); // adjust config auto group = kwinApp()->config()->group("Windows"); group.writeEntry("BorderlessMaximizedWindows", false); group.sync(); Workspace::self()->slotReconfigure(); QCOMPARE(options->borderlessMaximizedWindows(), false); } void TestMaximized::testMaximizedPassedToDeco() { // this test verifies that when a XdgShellClient gets maximized the Decoration receives the signal // Create the test client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QScopedPointer ssd(Test::waylandServerSideDecoration()->create(surface.data())); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isDecorated()); auto decoration = client->decoration(); QVERIFY(decoration); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); // Wait for configure event that signals the client is active now. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); // When there are no borders, there is no change to them when maximizing. // TODO: we should test both cases with fixed fake decoration for autotests. const bool hasBorders = Decoration::DecorationBridge::self()->settings()->borderSize() != KDecoration2::BorderSize::None; // now maximize QSignalSpy bordersChangedSpy(decoration, &KDecoration2::Decoration::bordersChanged); QVERIFY(bordersChangedSpy.isValid()); QSignalSpy maximizedChangedSpy(decoration->client().data(), &KDecoration2::DecoratedClient::maximizedChanged); QVERIFY(maximizedChangedSpy.isValid()); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024 - decoration->borderTop())); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), configureRequestedSpy.last().at(0).toSize(), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); // If no borders, there is only the initial geometry shape change, but none through border resizing. QCOMPARE(frameGeometryChangedSpy.count(), hasBorders ? 2 : 1); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(maximizedChangedSpy.count(), 1); QCOMPARE(maximizedChangedSpy.last().first().toBool(), true); QCOMPARE(bordersChangedSpy.count(), hasBorders ? 1 : 0); QCOMPARE(decoration->borderLeft(), 0); QCOMPARE(decoration->borderBottom(), 0); QCOMPARE(decoration->borderRight(), 0); QVERIFY(decoration->borderTop() != 0); // now unmaximize again workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), hasBorders ? 4 : 2); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(maximizedChangedSpy.count(), 2); QCOMPARE(maximizedChangedSpy.last().first().toBool(), false); QCOMPARE(bordersChangedSpy.count(), hasBorders ? 2 : 0); QVERIFY(decoration->borderTop() != 0); QVERIFY(decoration->borderLeft() != !hasBorders); QVERIFY(decoration->borderRight() != !hasBorders); QVERIFY(decoration->borderBottom() != !hasBorders); // Destroy the test client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestMaximized::testInitiallyMaximized() { // This test verifies that a window created as maximized, will be maximized. // Create the test client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface( Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); shellSurface->setMaximized(true); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Now let's render in an incorrect size. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QCOMPARE(client->frameGeometry(), QRect(0, 0, 100, 50)); QEXPECT_FAIL("", "Should go out of maximzied", Continue); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); // Destroy the client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestMaximized::testInitiallyMaximizedBorderless() { // This test verifies that a window created as maximized, will be maximized and without Border with BorderlessMaximizedWindows // adjust config auto group = kwinApp()->config()->group("Windows"); group.writeEntry("BorderlessMaximizedWindows", true); group.sync(); Workspace::self()->slotReconfigure(); QCOMPARE(options->borderlessMaximizedWindows(), true); // Create the test client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface( Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer decoration( Test::xdgDecorationManager()->getToplevelDecoration(shellSurface.data())); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); shellSurface->setMaximized(true); QSignalSpy decorationConfiguredSpy(decoration.data(), &XdgDecoration::modeChanged); QVERIFY(decorationConfiguredSpy.isValid()); decoration->setMode(XdgDecoration::Mode::ServerSide); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(!client->isDecorated()); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); QCOMPARE(decoration->mode(), XdgDecoration::Mode::ServerSide); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestMaximized::testBorderlessMaximizedWindow() { // This test verifies that a maximized client looses it's server-side // decoration when the borderless maximized option is on. // Enable the borderless maximized windows option. auto group = kwinApp()->config()->group("Windows"); group.writeEntry("BorderlessMaximizedWindows", true); group.sync(); Workspace::self()->slotReconfigure(); QCOMPARE(options->borderlessMaximizedWindows(), true); // Create the test client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface( Test::createXdgShellStableSurface(surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer decoration( Test::xdgDecorationManager()->getToplevelDecoration(shellSurface.data())); QSignalSpy decorationConfiguredSpy(decoration.data(), &XdgDecoration::modeChanged); QVERIFY(decorationConfiguredSpy.isValid()); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); decoration->setMode(XdgDecoration::Mode::ServerSide); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->isDecorated(), true); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Maximize the client. const QRect maximizeRestoreGeometry = client->frameGeometry(); workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->isDecorated(), false); // Restore the client. workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 4); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), maximizeRestoreGeometry); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->isDecorated(), true); // Destroy the client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestMaximized::testBorderlessMaximizedWindowNoClientSideDecoration() { // test case verifies that borderless maximized windows doesn't cause // clients to render client-side decorations instead (BUG 405385) + XdgShellSurface::States states; + // adjust config auto group = kwinApp()->config()->group("Windows"); group.writeEntry("BorderlessMaximizedWindows", true); group.sync(); Workspace::self()->slotReconfigure(); QCOMPARE(options->borderlessMaximizedWindows(), true); QScopedPointer surface(Test::createSurface()); QScopedPointer xdgShellSurface(Test::createXdgShellStableSurface(surface.data())); QScopedPointer deco(Test::xdgDecorationManager()->getToplevelDecoration(xdgShellSurface.data())); QSignalSpy decorationConfiguredSpy(deco.data(), &XdgDecoration::modeChanged); QVERIFY(decorationConfiguredSpy.isValid()); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client->isDecorated()); + QVERIFY(!client->noBorder()); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); - QSignalSpy sizeChangeRequestedSpy(xdgShellSurface.data(), &XdgShellSurface::sizeChanged); - QVERIFY(sizeChangeRequestedSpy.isValid()); QSignalSpy configureRequestedSpy(xdgShellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); - QVERIFY(client->isDecorated()); - QVERIFY(!client->noBorder()); - configureRequestedSpy.wait(); + // Wait for the compositor to send a configure event with the Activated state. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Activated); + QCOMPARE(decorationConfiguredSpy.count(), 1); QCOMPARE(deco->mode(), XdgDecoration::Mode::ServerSide); // go to maximized xdgShellSurface->setMaximized(true); - QVERIFY(sizeChangeRequestedSpy.wait()); - QCOMPARE(sizeChangeRequestedSpy.count(), 1); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Maximized); - for (const auto &it: configureRequestedSpy) { - xdgShellSurface->ackConfigure(it[2].toInt()); - } - Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); + xdgShellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); // no deco QVERIFY(!client->isDecorated()); QVERIFY(client->noBorder()); // but still server-side QCOMPARE(deco->mode(), XdgDecoration::Mode::ServerSide); // go back to normal xdgShellSurface->setMaximized(false); - QVERIFY(sizeChangeRequestedSpy.wait()); - QCOMPARE(sizeChangeRequestedSpy.count(), 2); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!(states & XdgShellSurface::State::Maximized)); - for (const auto &it: configureRequestedSpy) { - xdgShellSurface->ackConfigure(it[2].toInt()); - } - Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); + xdgShellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); QVERIFY(client->isDecorated()); QVERIFY(!client->noBorder()); QCOMPARE(deco->mode(), XdgDecoration::Mode::ServerSide); } WAYLANDTEST_MAIN(TestMaximized) #include "maximize_test.moc" diff --git a/autotests/integration/move_resize_window_test.cpp b/autotests/integration/move_resize_window_test.cpp index d2fe89fe7..2bfde0750 100644 --- a/autotests/integration/move_resize_window_test.cpp +++ b/autotests/integration/move_resize_window_test.cpp @@ -1,1210 +1,1209 @@ /******************************************************************** 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 "x11client.h" #include "cursor.h" #include "effects.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include "deleted.h" #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(KWin::QuickTileMode) Q_DECLARE_METATYPE(KWin::MaximizeMode) namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_kwin_quick_tiling-0"); class MoveResizeWindowTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testMove(); void testResize(); void testPackTo_data(); void testPackTo(); void testPackAgainstClient_data(); void testPackAgainstClient(); void testGrowShrink_data(); void testGrowShrink(); void testPointerMoveEnd_data(); void testPointerMoveEnd(); void testClientSideMove_data(); void testClientSideMove(); void testPlasmaShellSurfaceMovable_data(); void testPlasmaShellSurfaceMovable(); void testNetMove(); void testAdjustClientGeometryOfAutohidingX11Panel_data(); void testAdjustClientGeometryOfAutohidingX11Panel(); void testAdjustClientGeometryOfAutohidingWaylandPanel_data(); void testAdjustClientGeometryOfAutohidingWaylandPanel(); void testResizeForVirtualKeyboard(); void testResizeForVirtualKeyboardWithMaximize(); void testResizeForVirtualKeyboardWithFullScreen(); void testDestroyMoveClient(); void testDestroyResizeClient(); void testUnmapMoveClient(); void testUnmapResizeClient(); private: KWayland::Client::ConnectionThread *m_connection = nullptr; KWayland::Client::Compositor *m_compositor = 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(); screens()->setCurrent(0); } void MoveResizeWindowTest::cleanup() { Test::destroyWaylandConnection(); } void MoveResizeWindowTest::testMove() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeSpy.isValid()); // let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized); QVERIFY(startMoveResizedSpy.isValid()); QSignalSpy moveResizedChangedSpy(c, &AbstractClient::moveResizedChanged); QVERIFY(moveResizedChangedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(c, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(c, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); // effects signal handlers QSignalSpy windowStartUserMovedResizedSpy(effects, &EffectsHandler::windowStartUserMovedResized); QVERIFY(windowStartUserMovedResizedSpy.isValid()); QSignalSpy windowStepUserMovedResizedSpy(effects, &EffectsHandler::windowStepUserMovedResized); QVERIFY(windowStepUserMovedResizedSpy.isValid()); QSignalSpy windowFinishUserMovedResizedSpy(effects, &EffectsHandler::windowFinishUserMovedResized); QVERIFY(windowFinishUserMovedResizedSpy.isValid()); // begin move QVERIFY(workspace()->moveResizeClient() == nullptr); QCOMPARE(c->isMove(), false); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), c); QCOMPARE(startMoveResizedSpy.count(), 1); QCOMPARE(moveResizedChangedSpy.count(), 1); QCOMPARE(windowStartUserMovedResizedSpy.count(), 1); QCOMPARE(c->isMove(), true); QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); // send some key events, not going through input redirection const QPoint cursorPos = Cursors::self()->mouse()->pos(); c->keyPressEvent(Qt::Key_Right); c->updateMoveResize(Cursors::self()->mouse()->pos()); QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); QEXPECT_FAIL("", "First event is ignored", Continue); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); clientStepUserMovedResizedSpy.clear(); windowStepUserMovedResizedSpy.clear(); c->keyPressEvent(Qt::Key_Right); c->updateMoveResize(Cursors::self()->mouse()->pos()); QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(16, 0)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(windowStepUserMovedResizedSpy.count(), 1); c->keyPressEvent(Qt::Key_Down | Qt::ALT); c->updateMoveResize(Cursors::self()->mouse()->pos()); QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); QCOMPARE(windowStepUserMovedResizedSpy.count(), 2); QCOMPARE(c->frameGeometry(), QRect(16, 32, 100, 50)); QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(16, 32)); // let's end QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); c->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(moveResizedChangedSpy.count(), 2); QCOMPARE(windowFinishUserMovedResizedSpy.count(), 1); QCOMPARE(c->frameGeometry(), QRect(16, 32, 100, 50)); QCOMPARE(c->isMove(), false); QVERIFY(workspace()->moveResizeClient() == nullptr); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); } void MoveResizeWindowTest::testResize() { // a test case which manually resizes a window using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface( surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QVERIFY(!shellSurface.isNull()); // Wait for the initial configure event. XdgShellSurface::States states; QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // Let's render. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QSignalSpy surfaceSizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(surfaceSizeChangedSpy.isValid()); // We have to receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy startMoveResizedSpy(c, &AbstractClient::clientStartUserMovedResized); QVERIFY(startMoveResizedSpy.isValid()); QSignalSpy moveResizedChangedSpy(c, &AbstractClient::moveResizedChanged); QVERIFY(moveResizedChangedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(c, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(c, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); // begin resize QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(c->isMove(), false); QCOMPARE(c->isResize(), false); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), c); QCOMPARE(startMoveResizedSpy.count(), 1); QCOMPARE(moveResizedChangedSpy.count(), 1); QCOMPARE(c->isResize(), true); QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); // Trigger a change. const QPoint cursorPos = Cursors::self()->mouse()->pos(); c->keyPressEvent(Qt::Key_Right); c->updateMoveResize(Cursors::self()->mouse()->pos()); QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); // The client should receive a configure event with the new size. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 4); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(surfaceSizeChangedSpy.count(), 1); QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(108, 50)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); // Now render new size. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(108, 50), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(c->frameGeometry(), QRect(0, 0, 108, 50)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); // Go down. c->keyPressEvent(Qt::Key_Down); c->updateMoveResize(Cursors::self()->mouse()->pos()); QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 8)); // The client should receive another configure event. QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 5); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(surfaceSizeChangedSpy.count(), 2); QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(108, 58)); // Now render new size. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(108, 58), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(c->frameGeometry(), QRect(0, 0, 108, 58)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); // Let's finalize the resize operation. QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); c->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(moveResizedChangedSpy.count(), 2); QCOMPARE(c->isResize(), false); QCOMPARE(workspace()->moveResizeClient(), nullptr); - QEXPECT_FAIL("", "XdgShellClient currently doesn't send final configure event", Abort); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 6); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // Destroy the client. - surface.reset(); + shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); } void MoveResizeWindowTest::testPackTo_data() { QTest::addColumn("methodCall"); QTest::addColumn("expectedGeometry"); QTest::newRow("left") << QStringLiteral("slotWindowPackLeft") << QRect(0, 487, 100, 50); QTest::newRow("up") << QStringLiteral("slotWindowPackUp") << QRect(590, 0, 100, 50); QTest::newRow("right") << QStringLiteral("slotWindowPackRight") << QRect(1180, 487, 100, 50); QTest::newRow("down") << QStringLiteral("slotWindowPackDown") << QRect(590, 974, 100, 50); } void MoveResizeWindowTest::testPackTo() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeSpy.isValid()); // let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); // let's place it centered Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); QCOMPARE(c->frameGeometry(), QRect(590, 487, 100, 50)); QFETCH(QString, methodCall); QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); QTEST(c->frameGeometry(), "expectedGeometry"); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); } void MoveResizeWindowTest::testPackAgainstClient_data() { QTest::addColumn("methodCall"); QTest::addColumn("expectedGeometry"); QTest::newRow("left") << QStringLiteral("slotWindowPackLeft") << QRect(10, 487, 100, 50); QTest::newRow("up") << QStringLiteral("slotWindowPackUp") << QRect(590, 10, 100, 50); QTest::newRow("right") << QStringLiteral("slotWindowPackRight") << QRect(1170, 487, 100, 50); QTest::newRow("down") << QStringLiteral("slotWindowPackDown") << QRect(590, 964, 100, 50); } void MoveResizeWindowTest::testPackAgainstClient() { using namespace KWayland::Client; QScopedPointer surface1(Test::createSurface()); QVERIFY(!surface1.isNull()); QScopedPointer surface2(Test::createSurface()); QVERIFY(!surface2.isNull()); QScopedPointer surface3(Test::createSurface()); QVERIFY(!surface3.isNull()); QScopedPointer surface4(Test::createSurface()); QVERIFY(!surface4.isNull()); QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); QVERIFY(!shellSurface1.isNull()); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); QVERIFY(!shellSurface2.isNull()); QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); QVERIFY(!shellSurface3.isNull()); QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); QVERIFY(!shellSurface4.isNull()); auto renderWindow = [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->frameGeometry().size(), QSize(10, 10)); // let's place it centered Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); QCOMPARE(c->frameGeometry(), QRect(635, 507, 10, 10)); QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); QCOMPARE(c->frameGeometry(), expectedGeometry); }; renderWindow(surface1.data(), QStringLiteral("slotWindowPackLeft"), QRect(0, 507, 10, 10)); renderWindow(surface2.data(), QStringLiteral("slotWindowPackUp"), QRect(635, 0, 10, 10)); renderWindow(surface3.data(), QStringLiteral("slotWindowPackRight"), QRect(1270, 507, 10, 10)); renderWindow(surface4.data(), QStringLiteral("slotWindowPackDown"), QRect(635, 1014, 10, 10)); QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); // let's place it centered Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); QCOMPARE(c->frameGeometry(), QRect(590, 487, 100, 50)); QFETCH(QString, methodCall); QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); QTEST(c->frameGeometry(), "expectedGeometry"); } void MoveResizeWindowTest::testGrowShrink_data() { QTest::addColumn("methodCall"); QTest::addColumn("expectedGeometry"); QTest::newRow("grow vertical") << QStringLiteral("slotWindowGrowVertical") << QRect(590, 487, 100, 537); QTest::newRow("grow horizontal") << QStringLiteral("slotWindowGrowHorizontal") << QRect(590, 487, 690, 50); QTest::newRow("shrink vertical") << QStringLiteral("slotWindowShrinkVertical") << QRect(590, 487, 100, 23); QTest::newRow("shrink horizontal") << QStringLiteral("slotWindowShrinkHorizontal") << QRect(590, 487, 40, 50); } void MoveResizeWindowTest::testGrowShrink() { using namespace KWayland::Client; // block geometry helper QScopedPointer surface1(Test::createSurface()); QVERIFY(!surface1.isNull()); QScopedPointer shellSurface1(Test::createXdgShellStableSurface(surface1.data())); QVERIFY(!shellSurface1.isNull()); Test::render(surface1.data(), QSize(650, 514), Qt::blue); QVERIFY(Test::waitForWaylandWindowShown()); workspace()->slotWindowPackRight(); workspace()->slotWindowPackDown(); QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeSpy.isValid()); // let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); // let's place it centered Placement::self()->placeCentered(c, QRect(0, 0, 1280, 1024)); QCOMPARE(c->frameGeometry(), QRect(590, 487, 100, 50)); QFETCH(QString, methodCall); QMetaObject::invokeMethod(workspace(), methodCall.toLocal8Bit().constData()); QVERIFY(sizeChangeSpy.wait()); Test::render(surface.data(), shellSurface->size(), Qt::red); QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); m_connection->flush(); QVERIFY(frameGeometryChangedSpy.wait()); QTEST(c->frameGeometry(), "expectedGeometry"); } void MoveResizeWindowTest::testPointerMoveEnd_data() { QTest::addColumn("additionalButton"); QTest::newRow("BTN_RIGHT") << BTN_RIGHT; QTest::newRow("BTN_MIDDLE") << BTN_MIDDLE; QTest::newRow("BTN_SIDE") << BTN_SIDE; QTest::newRow("BTN_EXTRA") << BTN_EXTRA; QTest::newRow("BTN_FORWARD") << BTN_FORWARD; QTest::newRow("BTN_BACK") << BTN_BACK; QTest::newRow("BTN_TASK") << BTN_TASK; for (int i=BTN_TASK + 1; i < BTN_JOYSTICK; i++) { QTest::newRow(QByteArray::number(i, 16).constData()) << i; } } void MoveResizeWindowTest::testPointerMoveEnd() { // this test verifies that moving a window through pointer only ends if all buttons are released using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeSpy.isValid()); // let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c, workspace()->activeClient()); QVERIFY(!c->isMove()); // let's trigger the left button quint32 timestamp = 1; kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(!c->isMove()); workspace()->slotWindowMove(); QVERIFY(c->isMove()); // let's press another button QFETCH(int, additionalButton); kwinApp()->platform()->pointerButtonPressed(additionalButton, timestamp++); QVERIFY(c->isMove()); // release the left button, should still have the window moving kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(c->isMove()); // but releasing the other button should now end moving kwinApp()->platform()->pointerButtonReleased(additionalButton, timestamp++); QVERIFY(!c->isMove()); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); } void MoveResizeWindowTest::testClientSideMove_data() { QTest::addColumn("type"); QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void MoveResizeWindowTest::testClientSideMove() { using namespace KWayland::Client; Cursors::self()->mouse()->setPos(640, 512); QScopedPointer pointer(Test::waylandSeat()->createPointer()); QSignalSpy pointerEnteredSpy(pointer.data(), &Pointer::entered); QVERIFY(pointerEnteredSpy.isValid()); QSignalSpy pointerLeftSpy(pointer.data(), &Pointer::left); QVERIFY(pointerLeftSpy.isValid()); QSignalSpy buttonSpy(pointer.data(), &Pointer::buttonStateChanged); QVERIFY(buttonSpy.isValid()); QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(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->frameGeometry(); Cursors::self()->mouse()->setPos(startGeometry.center()); QVERIFY(pointerEnteredSpy.wait()); QCOMPARE(pointerEnteredSpy.first().last().toPoint(), QPoint(49, 24)); // simulate press quint32 timestamp = 1; kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(buttonSpy.wait()); QSignalSpy moveStartSpy(c, &AbstractClient::clientStartUserMovedResized); QVERIFY(moveStartSpy.isValid()); shellSurface->requestMove(Test::waylandSeat(), buttonSpy.first().first().value()); QVERIFY(moveStartSpy.wait()); QCOMPARE(c->isMove(), true); QVERIFY(pointerLeftSpy.wait()); // move a bit QSignalSpy clientMoveStepSpy(c, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientMoveStepSpy.isValid()); const QPoint startPoint = startGeometry.center(); const int dragDistance = QApplication::startDragDistance(); // Why? kwinApp()->platform()->pointerMotion(startPoint + QPoint(dragDistance, dragDistance) + QPoint(6, 6), timestamp++); QCOMPARE(clientMoveStepSpy.count(), 1); // and release again kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(pointerEnteredSpy.wait()); QCOMPARE(c->isMove(), false); QCOMPARE(c->frameGeometry(), startGeometry.translated(QPoint(dragDistance, dragDistance) + QPoint(6, 6))); QCOMPARE(pointerEnteredSpy.last().last().toPoint(), QPoint(49, 24)); } void MoveResizeWindowTest::testPlasmaShellSurfaceMovable_data() { QTest::addColumn("role"); QTest::addColumn("movable"); QTest::addColumn("movableAcrossScreens"); QTest::addColumn("resizable"); QTest::newRow("normal") << KWayland::Client::PlasmaShellSurface::Role::Normal << true << true << true; QTest::newRow("desktop") << KWayland::Client::PlasmaShellSurface::Role::Desktop << false << false << false; QTest::newRow("panel") << KWayland::Client::PlasmaShellSurface::Role::Panel << false << false << false; QTest::newRow("osd") << KWayland::Client::PlasmaShellSurface::Role::OnScreenDisplay << false << false << false; } void MoveResizeWindowTest::testPlasmaShellSurfaceMovable() { // this test verifies that certain window types from PlasmaShellSurface are not moveable or resizable using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // and a PlasmaShellSurface QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(surface.data())); QVERIFY(!plasmaSurface.isNull()); QFETCH(KWayland::Client::PlasmaShellSurface::Role, role); plasmaSurface->setRole(role); // let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QTEST(c->isMovable(), "movable"); QTEST(c->isMovableAcrossScreens(), "movableAcrossScreens"); QTEST(c->isResizable(), "resizable"); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); } struct XcbConnectionDeleter { static inline void cleanup(xcb_connection_t *pointer) { xcb_disconnect(pointer); } }; void MoveResizeWindowTest::testNetMove() { // this test verifies that a move request for an X11 window through NET API works // create an xcb window QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); xcb_window_t w = xcb_generate_id(c.data()); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), 0, 0, 100, 100, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, 0, 0); xcb_icccm_size_hints_set_size(&hints, 1, 100, 100); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); // let's set a no-border NETWinInfo winInfo(c.data(), w, rootWindow(), NET::WMWindowType, NET::Properties2()); winInfo.setWindowType(NET::Override); xcb_map_window(c.data(), w); xcb_flush(c.data()); QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Client *client = windowCreatedSpy.first().first().value(); QVERIFY(client); QCOMPARE(client->window(), w); const QRect origGeo = client->frameGeometry(); // let's move the cursor outside the window Cursors::self()->mouse()->setPos(screens()->geometry(0).center()); QVERIFY(!origGeo.contains(Cursors::self()->mouse()->pos())); QSignalSpy moveStartSpy(client, &X11Client::clientStartUserMovedResized); QVERIFY(moveStartSpy.isValid()); QSignalSpy moveEndSpy(client, &X11Client::clientFinishUserMovedResized); QVERIFY(moveEndSpy.isValid()); QSignalSpy moveStepSpy(client, &X11Client::clientStepUserMovedResized); QVERIFY(moveStepSpy.isValid()); QVERIFY(!workspace()->moveResizeClient()); // use NETRootInfo to trigger a move request NETRootInfo root(c.data(), NET::Properties()); root.moveResizeRequest(w, origGeo.center().x(), origGeo.center().y(), NET::Move); xcb_flush(c.data()); QVERIFY(moveStartSpy.wait()); QCOMPARE(workspace()->moveResizeClient(), client); QVERIFY(client->isMove()); QCOMPARE(client->geometryRestore(), origGeo); QCOMPARE(Cursors::self()->mouse()->pos(), origGeo.center()); // let's move a step Cursors::self()->mouse()->setPos(Cursors::self()->mouse()->pos() + QPoint(10, 10)); QCOMPARE(moveStepSpy.count(), 1); QCOMPARE(moveStepSpy.first().last().toRect(), origGeo.translated(10, 10)); // let's cancel the move resize again through the net API root.moveResizeRequest(w, client->frameGeometry().center().x(), client->frameGeometry().center().y(), NET::MoveResizeCancel); xcb_flush(c.data()); QVERIFY(moveEndSpy.wait()); // and destroy the window again xcb_unmap_window(c.data(), w); xcb_destroy_window(c.data(), w); xcb_flush(c.data()); c.reset(); QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); QVERIFY(windowClosedSpy.isValid()); QVERIFY(windowClosedSpy.wait()); } void MoveResizeWindowTest::testAdjustClientGeometryOfAutohidingX11Panel_data() { QTest::addColumn("panelGeometry"); QTest::addColumn("targetPoint"); QTest::addColumn("expectedAdjustedPoint"); QTest::addColumn("hideLocation"); QTest::newRow("top") << QRect(0, 0, 100, 20) << QPoint(50, 25) << QPoint(50, 20) << 0u; QTest::newRow("bottom") << QRect(0, 1024-20, 100, 20) << QPoint(50, 1024 - 25 - 50) << QPoint(50, 1024 - 20 - 50) << 2u; QTest::newRow("left") << QRect(0, 0, 20, 100) << QPoint(25, 50) << QPoint(20, 50) << 3u; QTest::newRow("right") << QRect(1280 - 20, 0, 20, 100) << QPoint(1280 - 25 - 100, 50) << QPoint(1280 - 20 - 100, 50) << 1u; } void MoveResizeWindowTest::testAdjustClientGeometryOfAutohidingX11Panel() { // this test verifies that auto hiding panels are ignored when adjusting client geometry // see BUG 365892 // first create our panel QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); xcb_window_t w = xcb_generate_id(c.data()); QFETCH(QRect, panelGeometry); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), panelGeometry.x(), panelGeometry.y(), panelGeometry.width(), panelGeometry.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, panelGeometry.x(), panelGeometry.y()); xcb_icccm_size_hints_set_size(&hints, 1, panelGeometry.width(), panelGeometry.height()); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); NETWinInfo winInfo(c.data(), w, rootWindow(), NET::WMWindowType, NET::Properties2()); winInfo.setWindowType(NET::Dock); xcb_map_window(c.data(), w); xcb_flush(c.data()); QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Client *panel = windowCreatedSpy.first().first().value(); QVERIFY(panel); QCOMPARE(panel->window(), w); QCOMPARE(panel->frameGeometry(), panelGeometry); QVERIFY(panel->isDock()); // let's create a window using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); auto testWindow = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(testWindow); QVERIFY(testWindow->isMovable()); // panel is not yet hidden, we should snap against it QFETCH(QPoint, targetPoint); QTEST(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), "expectedAdjustedPoint"); // now let's hide the panel QSignalSpy panelHiddenSpy(panel, &AbstractClient::windowHidden); QVERIFY(panelHiddenSpy.isValid()); QFETCH(quint32, hideLocation); xcb_change_property(c.data(), XCB_PROP_MODE_REPLACE, w, atoms->kde_screen_edge_show, XCB_ATOM_CARDINAL, 32, 1, &hideLocation); xcb_flush(c.data()); QVERIFY(panelHiddenSpy.wait()); // now try to snap again QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); // and destroy the panel again xcb_unmap_window(c.data(), w); xcb_destroy_window(c.data(), w); xcb_flush(c.data()); c.reset(); QSignalSpy panelClosedSpy(panel, &X11Client::windowClosed); QVERIFY(panelClosedSpy.isValid()); QVERIFY(panelClosedSpy.wait()); // snap once more QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); // and close QSignalSpy windowClosedSpy(testWindow, &AbstractClient::windowClosed); QVERIFY(windowClosedSpy.isValid()); shellSurface.reset(); surface.reset(); QVERIFY(windowClosedSpy.wait()); } void MoveResizeWindowTest::testAdjustClientGeometryOfAutohidingWaylandPanel_data() { QTest::addColumn("panelGeometry"); QTest::addColumn("targetPoint"); QTest::addColumn("expectedAdjustedPoint"); QTest::newRow("top") << QRect(0, 0, 100, 20) << QPoint(50, 25) << QPoint(50, 20); QTest::newRow("bottom") << QRect(0, 1024-20, 100, 20) << QPoint(50, 1024 - 25 - 50) << QPoint(50, 1024 - 20 - 50); QTest::newRow("left") << QRect(0, 0, 20, 100) << QPoint(25, 50) << QPoint(20, 50); QTest::newRow("right") << QRect(1280 - 20, 0, 20, 100) << QPoint(1280 - 25 - 100, 50) << QPoint(1280 - 20 - 100, 50); } void MoveResizeWindowTest::testAdjustClientGeometryOfAutohidingWaylandPanel() { // this test verifies that auto hiding panels are ignored when adjusting client geometry // see BUG 365892 // first create our panel using namespace KWayland::Client; QScopedPointer panelSurface(Test::createSurface()); QVERIFY(!panelSurface.isNull()); QScopedPointer panelShellSurface(Test::createXdgShellStableSurface(panelSurface.data())); QVERIFY(!panelShellSurface.isNull()); QScopedPointer plasmaSurface(Test::waylandPlasmaShell()->createSurface(panelSurface.data())); QVERIFY(!plasmaSurface.isNull()); plasmaSurface->setRole(PlasmaShellSurface::Role::Panel); plasmaSurface->setPanelBehavior(PlasmaShellSurface::PanelBehavior::AutoHide); QFETCH(QRect, panelGeometry); plasmaSurface->setPosition(panelGeometry.topLeft()); // let's render auto panel = Test::renderAndWaitForShown(panelSurface.data(), panelGeometry.size(), Qt::blue); QVERIFY(panel); QCOMPARE(panel->frameGeometry(), panelGeometry); QVERIFY(panel->isDock()); // let's create a window QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); auto testWindow = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(testWindow); QVERIFY(testWindow->isMovable()); // panel is not yet hidden, we should snap against it QFETCH(QPoint, targetPoint); QTEST(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), "expectedAdjustedPoint"); // now let's hide the panel QSignalSpy panelHiddenSpy(panel, &AbstractClient::windowHidden); QVERIFY(panelHiddenSpy.isValid()); plasmaSurface->requestHideAutoHidingPanel(); QVERIFY(panelHiddenSpy.wait()); // now try to snap again QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); // and destroy the panel again QSignalSpy panelClosedSpy(panel, &AbstractClient::windowClosed); QVERIFY(panelClosedSpy.isValid()); plasmaSurface.reset(); panelShellSurface.reset(); panelSurface.reset(); QVERIFY(panelClosedSpy.wait()); // snap once more QCOMPARE(Workspace::self()->adjustClientPosition(testWindow, targetPoint, false), targetPoint); // and close QSignalSpy windowClosedSpy(testWindow, &AbstractClient::windowClosed); QVERIFY(windowClosedSpy.isValid()); shellSurface.reset(); surface.reset(); QVERIFY(windowClosedSpy.wait()); } void MoveResizeWindowTest::testResizeForVirtualKeyboard() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // let's render auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); QVERIFY(client); // The client should receive a configure event upon becoming active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); client->move(100, 300); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); QCOMPARE(client->frameGeometry(), QRect(100, 300, 500, 800)); client->setVirtualKeyboardGeometry(QRect(0, 100, 1280, 500)); QVERIFY(configureRequestedSpy.wait()); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); // render at the new size Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(100, 0, 500, 101)); client->setVirtualKeyboardGeometry(QRect()); QVERIFY(configureRequestedSpy.wait()); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); // render at the new size Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(100, 300, 500, 800)); } void MoveResizeWindowTest::testResizeForVirtualKeyboardWithMaximize() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // let's render auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); QVERIFY(client); // The client should receive a configure event upon becoming active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); client->move(100, 300); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); QCOMPARE(client->frameGeometry(), QRect(100, 300, 500, 800)); client->setVirtualKeyboardGeometry(QRect(0, 100, 1280, 500)); QVERIFY(configureRequestedSpy.wait()); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); // render at the new size Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(100, 0, 500, 101)); client->setMaximize(true, true); QVERIFY(configureRequestedSpy.wait()); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); client->setVirtualKeyboardGeometry(QRect()); QVERIFY(!configureRequestedSpy.wait(10)); // render at the size of the configureRequested.. it won't have changed Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); QVERIFY(!frameGeometryChangedSpy.wait(10)); // Size will NOT be restored QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); } void MoveResizeWindowTest::testResizeForVirtualKeyboardWithFullScreen() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // let's render auto client = Test::renderAndWaitForShown(surface.data(), QSize(500, 800), Qt::blue); QVERIFY(client); // The client should receive a configure event upon becoming active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); client->move(100, 300); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); QCOMPARE(client->frameGeometry(), QRect(100, 300, 500, 800)); client->setVirtualKeyboardGeometry(QRect(0, 100, 1280, 500)); QVERIFY(configureRequestedSpy.wait()); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); // render at the new size Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(100, 0, 500, 101)); client->setFullScreen(true, true); QVERIFY(configureRequestedSpy.wait()); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); client->setVirtualKeyboardGeometry(QRect()); QVERIFY(!configureRequestedSpy.wait(10)); // render at the size of the configureRequested.. it won't have changed Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::blue); QVERIFY(!frameGeometryChangedSpy.wait(10)); // Size will NOT be restored QCOMPARE(client->frameGeometry(), QRect(0, 0, 1280, 1024)); } void MoveResizeWindowTest::testDestroyMoveClient() { // This test verifies that active move operation gets finished when // the associated client is destroyed. // Create the test client. using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); // Start moving the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), false); workspace()->slotWindowMove(); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(client->isMove(), true); QCOMPARE(client->isResize(), false); // Let's pretend that the client crashed. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); QCOMPARE(workspace()->moveResizeClient(), nullptr); } void MoveResizeWindowTest::testDestroyResizeClient() { // This test verifies that active resize operation gets finished when // the associated client is destroyed. // Create the test client. using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); // Start resizing the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), false); workspace()->slotWindowResize(); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), true); // Let's pretend that the client crashed. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); QCOMPARE(workspace()->moveResizeClient(), nullptr); } void MoveResizeWindowTest::testUnmapMoveClient() { // This test verifies that active move operation gets cancelled when // the associated client is unmapped. // Create the test client. using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); // Start resizing the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), false); workspace()->slotWindowMove(); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(client->isMove(), true); QCOMPARE(client->isResize(), false); // Unmap the client while we're moving it. QSignalSpy hiddenSpy(client, &AbstractClient::windowHidden); QVERIFY(hiddenSpy.isValid()); surface->attachBuffer(Buffer::Ptr()); surface->commit(Surface::CommitFlag::None); QVERIFY(hiddenSpy.wait()); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), false); // Destroy the client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); } void MoveResizeWindowTest::testUnmapResizeClient() { // This test verifies that active resize operation gets cancelled when // the associated client is unmapped. // Create the test client. using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); // Start resizing the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), false); workspace()->slotWindowResize(); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), true); // Unmap the client while we're resizing it. QSignalSpy hiddenSpy(client, &AbstractClient::windowHidden); QVERIFY(hiddenSpy.isValid()); surface->attachBuffer(Buffer::Ptr()); surface->commit(Surface::CommitFlag::None); QVERIFY(hiddenSpy.wait()); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(client->isMove(), false); QCOMPARE(client->isResize(), false); // Destroy the client. shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); } } WAYLANDTEST_MAIN(KWin::MoveResizeWindowTest) #include "move_resize_window_test.moc" diff --git a/autotests/integration/quick_tiling_test.cpp b/autotests/integration/quick_tiling_test.cpp index 7f48634e3..5e54977d4 100644 --- a/autotests/integration/quick_tiling_test.cpp +++ b/autotests/integration/quick_tiling_test.cpp @@ -1,890 +1,887 @@ /******************************************************************** 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 "x11client.h" #include "cursor.h" #include "decorations/decorationbridge.h" #include "decorations/settings.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include "scripting/scripting.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include Q_DECLARE_METATYPE(KWin::QuickTileMode) Q_DECLARE_METATYPE(KWin::MaximizeMode) namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_kwin_quick_tiling-0"); class QuickTilingTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testQuickTiling_data(); void testQuickTiling(); void testQuickMaximizing_data(); void testQuickMaximizing(); void testQuickTilingKeyboardMove_data(); void testQuickTilingKeyboardMove(); void testQuickTilingPointerMove_data(); void testQuickTilingPointerMove(); void testQuickTilingTouchMove_data(); void testQuickTilingTouchMove(); void testX11QuickTiling_data(); void testX11QuickTiling(); void testX11QuickTilingAfterVertMaximize_data(); void testX11QuickTilingAfterVertMaximize(); void testShortcut_data(); void testShortcut(); void testScript_data(); void testScript(); private: KWayland::Client::ConnectionThread *m_connection = nullptr; KWayland::Client::Compositor *m_compositor = nullptr; }; void QuickTilingTest::initTestCase() { qRegisterMetaType(); qRegisterMetaType("MaximizeMode"); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); // set custom config which disables the Outline KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); KConfigGroup group = config->group("Outline"); group.writeEntry(QStringLiteral("QmlPath"), QString("/does/not/exist.qml")); group.sync(); kwinApp()->setConfig(config); qputenv("XKB_DEFAULT_RULES", "evdev"); kwinApp()->start(); QVERIFY(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(); screens()->setCurrent(0); } void QuickTilingTest::cleanup() { Test::destroyWaylandConnection(); } void QuickTilingTest::testQuickTiling_data() { QTest::addColumn("mode"); QTest::addColumn("expectedGeometry"); QTest::addColumn("secondScreen"); QTest::addColumn("expectedModeAfterToggle"); #define FLAG(name) QuickTileMode(QuickTileFlag::name) QTest::newRow("left") << FLAG(Left) << QRect(0, 0, 640, 1024) << QRect(1280, 0, 640, 1024) << FLAG(Right); QTest::newRow("top") << FLAG(Top) << QRect(0, 0, 1280, 512) << QRect(1280, 0, 1280, 512) << FLAG(Top); QTest::newRow("right") << FLAG(Right) << QRect(640, 0, 640, 1024) << QRect(1920, 0, 640, 1024) << QuickTileMode(); QTest::newRow("bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512) << QRect(1280, 512, 1280, 512) << FLAG(Bottom); QTest::newRow("top left") << (FLAG(Left) | FLAG(Top)) << QRect(0, 0, 640, 512) << QRect(1280, 0, 640, 512) << (FLAG(Right) | FLAG(Top)); QTest::newRow("top right") << (FLAG(Right) | FLAG(Top)) << QRect(640, 0, 640, 512) << QRect(1920, 0, 640, 512) << QuickTileMode(); QTest::newRow("bottom left") << (FLAG(Left) | FLAG(Bottom)) << QRect(0, 512, 640, 512) << QRect(1280, 512, 640, 512) << (FLAG(Right) | FLAG(Bottom)); QTest::newRow("bottom right") << (FLAG(Right) | FLAG(Bottom)) << QRect(640, 512, 640, 512) << QRect(1920, 512, 640, 512) << QuickTileMode(); QTest::newRow("maximize") << FLAG(Maximize) << QRect(0, 0, 1280, 1024) << QRect(1280, 0, 1280, 1024) << QuickTileMode(); #undef FLAG } void QuickTilingTest::testQuickTiling() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // Map the client. auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); // We have to receive a configure event when the client becomes active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); QFETCH(QuickTileMode, mode); QFETCH(QRect, expectedGeometry); c->setQuickTileMode(mode, true); QCOMPARE(quickTileChangedSpy.count(), 1); // at this point the geometry did not yet change QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); // but quick tile mode already changed QCOMPARE(c->quickTileMode(), mode); // but we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); - QEXPECT_FAIL("maximize", "Two configure events are sent for maximized", Continue); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); // attach a new image shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), expectedGeometry.size(), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(c->frameGeometry(), expectedGeometry); // send window to other screen QCOMPARE(c->screen(), 0); c->sendToScreen(1); QCOMPARE(c->screen(), 1); // quick tile should not be changed QCOMPARE(c->quickTileMode(), mode); QTEST(c->frameGeometry(), "secondScreen"); // now try to toggle again c->setQuickTileMode(mode, true); QTEST(c->quickTileMode(), "expectedModeAfterToggle"); } void QuickTilingTest::testQuickMaximizing_data() { QTest::addColumn("mode"); #define FLAG(name) QuickTileMode(QuickTileFlag::name) QTest::newRow("maximize") << FLAG(Maximize); QTest::newRow("none") << FLAG(None); #undef FLAG } void QuickTilingTest::testQuickMaximizing() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // Map the client. auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); QCOMPARE(c->maximizeMode(), MaximizeRestore); // We have to receive a configure event upon becoming active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy maximizeChangedSpy1(c, qOverload(&AbstractClient::clientMaximizedStateChanged)); QVERIFY(maximizeChangedSpy1.isValid()); QSignalSpy maximizeChangedSpy2(c, qOverload(&AbstractClient::clientMaximizedStateChanged)); QVERIFY(maximizeChangedSpy2.isValid()); c->setQuickTileMode(QuickTileFlag::Maximize, true); QCOMPARE(quickTileChangedSpy.count(), 1); // at this point the geometry did not yet change QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); // but quick tile mode already changed QCOMPARE(c->quickTileMode(), QuickTileFlag::Maximize); QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); // but we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); - QEXPECT_FAIL("", "Two configure events are sent for maximized", Continue); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); // attach a new image shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(c->frameGeometry(), QRect(0, 0, 1280, 1024)); QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); // client is now set to maximised QCOMPARE(maximizeChangedSpy1.count(), 1); QCOMPARE(maximizeChangedSpy1.first().first().value(), c); QCOMPARE(maximizeChangedSpy1.first().last().value(), MaximizeFull); QCOMPARE(maximizeChangedSpy2.count(), 1); QCOMPARE(maximizeChangedSpy2.first().first().value(), c); QCOMPARE(maximizeChangedSpy2.first().at(1).toBool(), true); QCOMPARE(maximizeChangedSpy2.first().at(2).toBool(), true); QCOMPARE(c->maximizeMode(), MaximizeFull); // go back to quick tile none QFETCH(QuickTileMode, mode); c->setQuickTileMode(mode, true); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); QCOMPARE(quickTileChangedSpy.count(), 2); // geometry not yet changed QCOMPARE(c->frameGeometry(), QRect(0, 0, 1280, 1024)); QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); // we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); - QEXPECT_FAIL("", "Two configure events are sent for maximized", Continue); QCOMPARE(configureRequestedSpy.count(), 3); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); // render again shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::yellow); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 2); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); QCOMPARE(maximizeChangedSpy1.count(), 2); QCOMPARE(maximizeChangedSpy1.last().first().value(), c); QCOMPARE(maximizeChangedSpy1.last().last().value(), MaximizeRestore); QCOMPARE(maximizeChangedSpy2.count(), 2); QCOMPARE(maximizeChangedSpy2.last().first().value(), c); QCOMPARE(maximizeChangedSpy2.last().at(1).toBool(), false); QCOMPARE(maximizeChangedSpy2.last().at(2).toBool(), false); } void QuickTilingTest::testQuickTilingKeyboardMove_data() { QTest::addColumn("targetPos"); QTest::addColumn("expectedMode"); QTest::newRow("topRight") << QPoint(2559, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Right); QTest::newRow("right") << QPoint(2559, 512) << QuickTileMode(QuickTileFlag::Right); QTest::newRow("bottomRight") << QPoint(2559, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Right); QTest::newRow("bottomLeft") << QPoint(0, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Left); QTest::newRow("Left") << QPoint(0, 512) << QuickTileMode(QuickTileFlag::Left); QTest::newRow("topLeft") << QPoint(0, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Left); } void QuickTilingTest::testQuickTilingKeyboardMove() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); QSignalSpy sizeChangeSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(sizeChangeSpy.isValid()); // let's render auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); QCOMPARE(c->maximizeMode(), MaximizeRestore); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); workspace()->performWindowOperation(c, Options::UnrestrictedMoveOp); QCOMPARE(c, workspace()->moveResizeClient()); QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(49, 24)); QFETCH(QPoint, targetPos); quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); while (Cursors::self()->mouse()->pos().x() > targetPos.x()) { kwinApp()->platform()->keyboardKeyPressed(KEY_LEFT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFT, timestamp++); } while (Cursors::self()->mouse()->pos().x() < targetPos.x()) { kwinApp()->platform()->keyboardKeyPressed(KEY_RIGHT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_RIGHT, timestamp++); } while (Cursors::self()->mouse()->pos().y() < targetPos.y()) { kwinApp()->platform()->keyboardKeyPressed(KEY_DOWN, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_DOWN, timestamp++); } while (Cursors::self()->mouse()->pos().y() > targetPos.y()) { kwinApp()->platform()->keyboardKeyPressed(KEY_UP, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_UP, timestamp++); } kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_ENTER, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_ENTER, timestamp++); QCOMPARE(Cursors::self()->mouse()->pos(), targetPos); QVERIFY(!workspace()->moveResizeClient()); QCOMPARE(quickTileChangedSpy.count(), 1); QTEST(c->quickTileMode(), "expectedMode"); } void QuickTilingTest::testQuickTilingPointerMove_data() { QTest::addColumn("targetPos"); QTest::addColumn("expectedMode"); QTest::newRow("topRight") << QPoint(2559, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Right); QTest::newRow("right") << QPoint(2559, 512) << QuickTileMode(QuickTileFlag::Right); QTest::newRow("bottomRight") << QPoint(2559, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Right); QTest::newRow("bottomLeft") << QPoint(0, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Left); QTest::newRow("Left") << QPoint(0, 512) << QuickTileMode(QuickTileFlag::Left); QTest::newRow("topLeft") << QPoint(0, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Left); } void QuickTilingTest::testQuickTilingPointerMove() { using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface( surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QVERIFY(!shellSurface.isNull()); // wait for the initial configure event QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); // let's render shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); QCOMPARE(c->maximizeMode(), MaximizeRestore); // we have to receive a configure event when the client becomes active QVERIFY(configureRequestedSpy.wait()); QTRY_COMPARE(configureRequestedSpy.count(), 2); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); workspace()->performWindowOperation(c, Options::UnrestrictedMoveOp); QCOMPARE(c, workspace()->moveResizeClient()); QCOMPARE(Cursors::self()->mouse()->pos(), QPoint(49, 24)); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); QFETCH(QPoint, targetPos); quint32 timestamp = 1; kwinApp()->platform()->pointerMotion(targetPos, timestamp++); kwinApp()->platform()->pointerButtonPressed(BTN_LEFT, timestamp++); kwinApp()->platform()->pointerButtonReleased(BTN_LEFT, timestamp++); QCOMPARE(Cursors::self()->mouse()->pos(), targetPos); QVERIFY(!workspace()->moveResizeClient()); QCOMPARE(quickTileChangedSpy.count(), 1); QTEST(c->quickTileMode(), "expectedMode"); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 4); QCOMPARE(false, configureRequestedSpy.last().first().toSize().isEmpty()); } void QuickTilingTest::testQuickTilingTouchMove_data() { QTest::addColumn("targetPos"); QTest::addColumn("expectedMode"); QTest::newRow("topRight") << QPoint(2559, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Right); QTest::newRow("right") << QPoint(2559, 512) << QuickTileMode(QuickTileFlag::Right); QTest::newRow("bottomRight") << QPoint(2559, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Right); QTest::newRow("bottomLeft") << QPoint(0, 1023) << QuickTileMode(QuickTileFlag::Bottom | QuickTileFlag::Left); QTest::newRow("Left") << QPoint(0, 512) << QuickTileMode(QuickTileFlag::Left); QTest::newRow("topLeft") << QPoint(0, 24) << QuickTileMode(QuickTileFlag::Top | QuickTileFlag::Left); } void QuickTilingTest::testQuickTilingTouchMove() { // test verifies that touch on decoration also allows quick tiling // see BUG: 390113 using namespace KWayland::Client; QScopedPointer surface(Test::createSurface()); QVERIFY(!surface.isNull()); QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); QScopedPointer shellSurface(Test::createXdgShellStableSurface( surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QVERIFY(!shellSurface.isNull()); // wait for the initial configure event QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); // let's render shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(1000, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isDecorated()); const auto decoration = c->decoration(); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(-decoration->borderLeft(), 0, 1000 + decoration->borderLeft() + decoration->borderRight(), 50 + decoration->borderTop() + decoration->borderBottom())); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); QCOMPARE(c->maximizeMode(), MaximizeRestore); // we have to receive a configure event when the client becomes active QVERIFY(configureRequestedSpy.wait()); QTRY_COMPARE(configureRequestedSpy.count(), 2); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); quint32 timestamp = 1; kwinApp()->platform()->touchDown(0, QPointF(c->frameGeometry().center().x(), c->frameGeometry().y() + decoration->borderTop() / 2), timestamp++); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(c, workspace()->moveResizeClient()); QCOMPARE(configureRequestedSpy.count(), 3); QFETCH(QPoint, targetPos); kwinApp()->platform()->touchMotion(0, targetPos, timestamp++); kwinApp()->platform()->touchUp(0, timestamp++); QVERIFY(!workspace()->moveResizeClient()); // When there are no borders, there is no change to them when quick-tiling. // TODO: we should test both cases with fixed fake decoration for autotests. const bool hasBorders = Decoration::DecorationBridge::self()->settings()->borderSize() != KDecoration2::BorderSize::None; QCOMPARE(quickTileChangedSpy.count(), 1); QTEST(c->quickTileMode(), "expectedMode"); QVERIFY(configureRequestedSpy.wait()); QTRY_COMPARE(configureRequestedSpy.count(), hasBorders ? 5 : 4); QCOMPARE(false, configureRequestedSpy.last().first().toSize().isEmpty()); } struct XcbConnectionDeleter { static inline void cleanup(xcb_connection_t *pointer) { xcb_disconnect(pointer); } }; void QuickTilingTest::testX11QuickTiling_data() { QTest::addColumn("mode"); QTest::addColumn("expectedGeometry"); QTest::addColumn("screen"); QTest::addColumn("modeAfterToggle"); #define FLAG(name) QuickTileMode(QuickTileFlag::name) QTest::newRow("left") << FLAG(Left) << QRect(0, 0, 640, 1024) << 0 << QuickTileMode(); QTest::newRow("top") << FLAG(Top) << QRect(0, 0, 1280, 512) << 1 << FLAG(Top); QTest::newRow("right") << FLAG(Right) << QRect(640, 0, 640, 1024) << 1 << FLAG(Left); QTest::newRow("bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512) << 1 << FLAG(Bottom); QTest::newRow("top left") << (FLAG(Left) | FLAG(Top)) << QRect(0, 0, 640, 512) << 0 << QuickTileMode(); QTest::newRow("top right") << (FLAG(Right) | FLAG(Top)) << QRect(640, 0, 640, 512) << 1 << (FLAG(Left) | FLAG(Top)); QTest::newRow("bottom left") << (FLAG(Left) | FLAG(Bottom)) << QRect(0, 512, 640, 512) << 0 << QuickTileMode(); QTest::newRow("bottom right") << (FLAG(Right) | FLAG(Bottom)) << QRect(640, 512, 640, 512) << 1 << (FLAG(Left) | FLAG(Bottom)); QTest::newRow("maximize") << FLAG(Maximize) << QRect(0, 0, 1280, 1024) << 0 << QuickTileMode(); #undef FLAG } void QuickTilingTest::testX11QuickTiling() { QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); const QRect windowGeometry(0, 0, 100, 200); xcb_window_t w = xcb_generate_id(c.data()); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), windowGeometry.x(), windowGeometry.y(), windowGeometry.width(), windowGeometry.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); xcb_map_window(c.data(), w); xcb_flush(c.data()); // we should get a client for it QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Client *client = windowCreatedSpy.first().first().value(); QVERIFY(client); QCOMPARE(client->window(), w); // now quick tile QSignalSpy quickTileChangedSpy(client, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); const QRect origGeo = client->frameGeometry(); QFETCH(QuickTileMode, mode); client->setQuickTileMode(mode, true); QCOMPARE(client->quickTileMode(), mode); QTEST(client->frameGeometry(), "expectedGeometry"); QCOMPARE(client->geometryRestore(), origGeo); QEXPECT_FAIL("maximize", "For maximize we get two changed signals", Continue); QCOMPARE(quickTileChangedSpy.count(), 1); // quick tile to same edge again should also act like send to screen QCOMPARE(client->screen(), 0); client->setQuickTileMode(mode, true); QTEST(client->screen(), "screen"); QTEST(client->quickTileMode(), "modeAfterToggle"); QCOMPARE(client->geometryRestore(), origGeo); // and destroy the window again xcb_unmap_window(c.data(), w); xcb_destroy_window(c.data(), w); xcb_flush(c.data()); c.reset(); QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); QVERIFY(windowClosedSpy.isValid()); QVERIFY(windowClosedSpy.wait()); } void QuickTilingTest::testX11QuickTilingAfterVertMaximize_data() { QTest::addColumn("mode"); QTest::addColumn("expectedGeometry"); #define FLAG(name) QuickTileMode(QuickTileFlag::name) QTest::newRow("left") << FLAG(Left) << QRect(0, 0, 640, 1024); QTest::newRow("top") << FLAG(Top) << QRect(0, 0, 1280, 512); QTest::newRow("right") << FLAG(Right) << QRect(640, 0, 640, 1024); QTest::newRow("bottom") << FLAG(Bottom) << QRect(0, 512, 1280, 512); QTest::newRow("top left") << (FLAG(Left) | FLAG(Top)) << QRect(0, 0, 640, 512); QTest::newRow("top right") << (FLAG(Right) | FLAG(Top)) << QRect(640, 0, 640, 512); QTest::newRow("bottom left") << (FLAG(Left) | FLAG(Bottom)) << QRect(0, 512, 640, 512); QTest::newRow("bottom right") << (FLAG(Right) | FLAG(Bottom)) << QRect(640, 512, 640, 512); QTest::newRow("maximize") << FLAG(Maximize) << QRect(0, 0, 1280, 1024); #undef FLAG } void QuickTilingTest::testX11QuickTilingAfterVertMaximize() { QScopedPointer c(xcb_connect(nullptr, nullptr)); QVERIFY(!xcb_connection_has_error(c.data())); const QRect windowGeometry(0, 0, 100, 200); xcb_window_t w = xcb_generate_id(c.data()); xcb_create_window(c.data(), XCB_COPY_FROM_PARENT, w, rootWindow(), windowGeometry.x(), windowGeometry.y(), windowGeometry.width(), windowGeometry.height(), 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); xcb_size_hints_t hints; memset(&hints, 0, sizeof(hints)); xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); xcb_icccm_set_wm_normal_hints(c.data(), w, &hints); xcb_map_window(c.data(), w); xcb_flush(c.data()); // we should get a client for it QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); QVERIFY(windowCreatedSpy.isValid()); QVERIFY(windowCreatedSpy.wait()); X11Client *client = windowCreatedSpy.first().first().value(); QVERIFY(client); QCOMPARE(client->window(), w); const QRect origGeo = client->frameGeometry(); QCOMPARE(client->maximizeMode(), MaximizeRestore); // vertically maximize the window client->maximize(client->maximizeMode() ^ MaximizeVertical); QCOMPARE(client->frameGeometry().width(), origGeo.width()); QCOMPARE(client->height(), screens()->size(client->screen()).height()); QCOMPARE(client->geometryRestore(), origGeo); // now quick tile QSignalSpy quickTileChangedSpy(client, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); QFETCH(QuickTileMode, mode); client->setQuickTileMode(mode, true); QCOMPARE(client->quickTileMode(), mode); QTEST(client->frameGeometry(), "expectedGeometry"); QEXPECT_FAIL("", "We get two changed events", Continue); QCOMPARE(quickTileChangedSpy.count(), 1); // and destroy the window again xcb_unmap_window(c.data(), w); xcb_destroy_window(c.data(), w); xcb_flush(c.data()); c.reset(); QSignalSpy windowClosedSpy(client, &X11Client::windowClosed); QVERIFY(windowClosedSpy.isValid()); QVERIFY(windowClosedSpy.wait()); } void QuickTilingTest::testShortcut_data() { QTest::addColumn("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::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // Map the client. auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); // We have to receive a configure event when the client becomes active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QFETCH(QString, shortcut); 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); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); QVERIFY(quickTileChangedSpy.wait()); QCOMPARE(quickTileChangedSpy.count(), 1); // at this point the geometry did not yet change QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); // but quick tile mode already changed QTEST(c->quickTileMode(), "expectedMode"); // but we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); // attach a new image QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), expectedGeometry.size(), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); QEXPECT_FAIL("maximize", "Geometry changed called twice for maximize", Continue); QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(c->frameGeometry(), expectedGeometry); } void QuickTilingTest::testScript_data() { QTest::addColumn("action"); QTest::addColumn("expectedMode"); QTest::addColumn("expectedGeometry"); #define FLAG(name) QuickTileMode(QuickTileFlag::name) QTest::newRow("top") << QStringLiteral("Top") << FLAG(Top) << QRect(0, 0, 1280, 512); QTest::newRow("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::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); // Map the client. auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(workspace()->activeClient(), c); QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); QCOMPARE(c->quickTileMode(), QuickTileMode(QuickTileFlag::None)); // We have to receive a configure event upon the client becoming active. QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QSignalSpy quickTileChangedSpy(c, &AbstractClient::quickTileModeChanged); QVERIFY(quickTileChangedSpy.isValid()); QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); QVERIFY(Scripting::self()); QTemporaryFile tmpFile; QVERIFY(tmpFile.open()); QTextStream out(&tmpFile); QFETCH(QString, action); out << "workspace.slotWindowQuickTile" << action << "()"; out.flush(); QFETCH(QuickTileMode, expectedMode); QFETCH(QRect, expectedGeometry); const int id = Scripting::self()->loadScript(tmpFile.fileName()); QVERIFY(id != -1); QVERIFY(Scripting::self()->isScriptLoaded(tmpFile.fileName())); auto s = Scripting::self()->findScript(tmpFile.fileName()); QVERIFY(s); QSignalSpy runningChangedSpy(s, &AbstractScript::runningChanged); QVERIFY(runningChangedSpy.isValid()); s->run(); QVERIFY(quickTileChangedSpy.wait()); QCOMPARE(quickTileChangedSpy.count(), 1); QCOMPARE(runningChangedSpy.count(), 1); QCOMPARE(runningChangedSpy.first().first().toBool(), true); // at this point the geometry did not yet change QCOMPARE(c->frameGeometry(), QRect(0, 0, 100, 50)); // but quick tile mode already changed QCOMPARE(c->quickTileMode(), expectedMode); // but we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); // attach a new image shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), expectedGeometry.size(), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); QEXPECT_FAIL("maximize", "Geometry changed called twice for maximize", Continue); QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(c->frameGeometry(), expectedGeometry); } } WAYLANDTEST_MAIN(KWin::QuickTilingTest) #include "quick_tiling_test.moc" diff --git a/autotests/integration/xdgshellclient_rules_test.cpp b/autotests/integration/xdgshellclient_rules_test.cpp index 53e5051fc..2b143ae92 100644 --- a/autotests/integration/xdgshellclient_rules_test.cpp +++ b/autotests/integration/xdgshellclient_rules_test.cpp @@ -1,4542 +1,4540 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2017 Martin Flöser Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 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 "abstract_client.h" #include "cursor.h" #include "platform.h" #include "rules.h" #include "screens.h" #include "virtualdesktops.h" #include "wayland_server.h" #include "workspace.h" #include #include #include using namespace KWin; using namespace KWayland::Client; static const QString s_socketName = QStringLiteral("wayland_test_kwin_xdgshellclient_rules-0"); class TestXdgShellClientRules : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testPositionDontAffect_data(); void testPositionDontAffect(); void testPositionApply_data(); void testPositionApply(); void testPositionRemember_data(); void testPositionRemember(); void testPositionForce_data(); void testPositionForce(); void testPositionApplyNow_data(); void testPositionApplyNow(); void testPositionForceTemporarily_data(); void testPositionForceTemporarily(); void testSizeDontAffect_data(); void testSizeDontAffect(); void testSizeApply_data(); void testSizeApply(); void testSizeRemember_data(); void testSizeRemember(); void testSizeForce_data(); void testSizeForce(); void testSizeApplyNow_data(); void testSizeApplyNow(); void testSizeForceTemporarily_data(); void testSizeForceTemporarily(); void testMaximizeDontAffect_data(); void testMaximizeDontAffect(); void testMaximizeApply_data(); void testMaximizeApply(); void testMaximizeRemember_data(); void testMaximizeRemember(); void testMaximizeForce_data(); void testMaximizeForce(); void testMaximizeApplyNow_data(); void testMaximizeApplyNow(); void testMaximizeForceTemporarily_data(); void testMaximizeForceTemporarily(); void testDesktopDontAffect_data(); void testDesktopDontAffect(); void testDesktopApply_data(); void testDesktopApply(); void testDesktopRemember_data(); void testDesktopRemember(); void testDesktopForce_data(); void testDesktopForce(); void testDesktopApplyNow_data(); void testDesktopApplyNow(); void testDesktopForceTemporarily_data(); void testDesktopForceTemporarily(); void testMinimizeDontAffect_data(); void testMinimizeDontAffect(); void testMinimizeApply_data(); void testMinimizeApply(); void testMinimizeRemember_data(); void testMinimizeRemember(); void testMinimizeForce_data(); void testMinimizeForce(); void testMinimizeApplyNow_data(); void testMinimizeApplyNow(); void testMinimizeForceTemporarily_data(); void testMinimizeForceTemporarily(); void testSkipTaskbarDontAffect_data(); void testSkipTaskbarDontAffect(); void testSkipTaskbarApply_data(); void testSkipTaskbarApply(); void testSkipTaskbarRemember_data(); void testSkipTaskbarRemember(); void testSkipTaskbarForce_data(); void testSkipTaskbarForce(); void testSkipTaskbarApplyNow_data(); void testSkipTaskbarApplyNow(); void testSkipTaskbarForceTemporarily_data(); void testSkipTaskbarForceTemporarily(); void testSkipPagerDontAffect_data(); void testSkipPagerDontAffect(); void testSkipPagerApply_data(); void testSkipPagerApply(); void testSkipPagerRemember_data(); void testSkipPagerRemember(); void testSkipPagerForce_data(); void testSkipPagerForce(); void testSkipPagerApplyNow_data(); void testSkipPagerApplyNow(); void testSkipPagerForceTemporarily_data(); void testSkipPagerForceTemporarily(); void testSkipSwitcherDontAffect_data(); void testSkipSwitcherDontAffect(); void testSkipSwitcherApply_data(); void testSkipSwitcherApply(); void testSkipSwitcherRemember_data(); void testSkipSwitcherRemember(); void testSkipSwitcherForce_data(); void testSkipSwitcherForce(); void testSkipSwitcherApplyNow_data(); void testSkipSwitcherApplyNow(); void testSkipSwitcherForceTemporarily_data(); void testSkipSwitcherForceTemporarily(); void testKeepAboveDontAffect_data(); void testKeepAboveDontAffect(); void testKeepAboveApply_data(); void testKeepAboveApply(); void testKeepAboveRemember_data(); void testKeepAboveRemember(); void testKeepAboveForce_data(); void testKeepAboveForce(); void testKeepAboveApplyNow_data(); void testKeepAboveApplyNow(); void testKeepAboveForceTemporarily_data(); void testKeepAboveForceTemporarily(); void testKeepBelowDontAffect_data(); void testKeepBelowDontAffect(); void testKeepBelowApply_data(); void testKeepBelowApply(); void testKeepBelowRemember_data(); void testKeepBelowRemember(); void testKeepBelowForce_data(); void testKeepBelowForce(); void testKeepBelowApplyNow_data(); void testKeepBelowApplyNow(); void testKeepBelowForceTemporarily_data(); void testKeepBelowForceTemporarily(); void testShortcutDontAffect_data(); void testShortcutDontAffect(); void testShortcutApply_data(); void testShortcutApply(); void testShortcutRemember_data(); void testShortcutRemember(); void testShortcutForce_data(); void testShortcutForce(); void testShortcutApplyNow_data(); void testShortcutApplyNow(); void testShortcutForceTemporarily_data(); void testShortcutForceTemporarily(); void testDesktopFileDontAffect_data(); void testDesktopFileDontAffect(); void testDesktopFileApply_data(); void testDesktopFileApply(); void testDesktopFileRemember_data(); void testDesktopFileRemember(); void testDesktopFileForce_data(); void testDesktopFileForce(); void testDesktopFileApplyNow_data(); void testDesktopFileApplyNow(); void testDesktopFileForceTemporarily_data(); void testDesktopFileForceTemporarily(); void testActiveOpacityDontAffect_data(); void testActiveOpacityDontAffect(); void testActiveOpacityForce_data(); void testActiveOpacityForce(); void testActiveOpacityForceTemporarily_data(); void testActiveOpacityForceTemporarily(); void testInactiveOpacityDontAffect_data(); void testInactiveOpacityDontAffect(); void testInactiveOpacityForce_data(); void testInactiveOpacityForce(); void testInactiveOpacityForceTemporarily_data(); void testInactiveOpacityForceTemporarily(); void testMatchAfterNameChange(); }; void TestXdgShellClientRules::initTestCase() { qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); kwinApp()->start(); QVERIFY(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 TestXdgShellClientRules::init() { VirtualDesktopManager::self()->setCurrent(VirtualDesktopManager::self()->desktops().first()); QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration)); screens()->setCurrent(0); } void TestXdgShellClientRules::cleanup() { Test::destroyWaylandConnection(); // Unreference the previous config. RuleBook::self()->setConfig({}); workspace()->slotReconfigure(); // Restore virtual desktops to the initial state. VirtualDesktopManager::self()->setCount(1); QCOMPARE(VirtualDesktopManager::self()->count(), 1u); } #define TEST_DATA(name) \ void TestXdgShellClientRules::name##_data() \ { \ QTest::addColumn("type"); \ QTest::newRow("XdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; \ } std::tuple createWindow(Test::XdgShellSurfaceType type, const QByteArray &appId) { // Create an xdg surface. Surface *surface = Test::createSurface(); XdgShellSurface *shellSurface = Test::createXdgShellSurface(type, surface, surface, Test::CreationSetup::CreateOnly); // Assign the desired app id. shellSurface->setAppId(appId); // Wait for the initial configure event. QSignalSpy configureRequestedSpy(shellSurface, &XdgShellSurface::configureRequested); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); // Draw content of the surface. shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface, QSize(100, 50), Qt::blue); return {client, surface, shellSurface}; } TEST_DATA(testPositionDontAffect) void TestXdgShellClientRules::testPositionDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The position of the client should not be affected by the rule. The default // placement policy will put the client in the top-left corner of the screen. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(0, 0)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionApply) void TestXdgShellClientRules::testPositionApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The client should be moved to the position specified by the rule. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // One should still be able to move the client around. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(client->isMove()); QVERIFY(!client->isResize()); const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(client->pos(), QPoint(50, 42)); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QCOMPARE(client->pos(), QPoint(50, 42)); // The rule should be applied again if the client appears after it's been closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionRemember) void TestXdgShellClientRules::testPositionRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The client should be moved to the position specified by the rule. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // One should still be able to move the client around. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(client->isMove()); QVERIFY(!client->isResize()); const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(client->pos(), QPoint(50, 42)); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QCOMPARE(client->pos(), QPoint(50, 42)); // The client should be placed at the last know position if we reopen it. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(50, 42)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionForce) void TestXdgShellClientRules::testPositionForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The client should be moved to the position specified by the rule. QVERIFY(!client->isMovable()); QVERIFY(!client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // User should not be able to move the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(clientStartMoveResizedSpy.count(), 0); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); // The position should still be forced if we reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isMovable()); QVERIFY(!client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionApplyNow) void TestXdgShellClientRules::testPositionApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; QObject *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The position of the client isn't set by any rule, thus the default placement // policy will try to put the client in the top-left corner of the screen. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(0, 0)); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); // The client should be moved to the position specified by the rule. QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); workspace()->slotReconfigure(); QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(client->pos(), QPoint(42, 42)); // We still have to be able to move the client around. QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(client->isMove()); QVERIFY(!client->isResize()); const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(client->pos(), QPoint(50, 42)); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QCOMPARE(client->pos(), QPoint(50, 42)); // The rule should not be applied again. client->evaluateWindowRules(); QCOMPARE(client->pos(), QPoint(50, 42)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testPositionForceTemporarily) void TestXdgShellClientRules::testPositionForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("position", QPoint(42, 42)); group.writeEntry("positionrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The client should be moved to the position specified by the rule. QVERIFY(!client->isMovable()); QVERIFY(!client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(42, 42)); // User should not be able to move the client. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowMove(); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(clientStartMoveResizedSpy.count(), 0); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); // The rule should be discarded if we close the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMovable()); QVERIFY(client->isMovableAcrossScreens()); QCOMPARE(client->pos(), QPoint(0, 0)); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeDontAffect) void TestXdgShellClientRules::testSizeDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The window size shouldn't be enforced by the rule. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(0, 0)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(100, 50)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeApply) void TestXdgShellClientRules::testSizeApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The initial configure event should contain size hint set by the rule. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(480, 640)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // One still should be able to resize the client. QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QSignalSpy surfaceSizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(surfaceSizeChangedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(!client->isMove()); QVERIFY(client->isResize()); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 4); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(surfaceSizeChangedSpy.count(), 1); QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(488, 640)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(488, 640), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(488, 640)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); - QEXPECT_FAIL("", "Interactive resize is not spec-compliant", Continue); QVERIFY(configureRequestedSpy->wait(10)); - QEXPECT_FAIL("", "Interactive resize is not spec-compliant", Continue); QCOMPARE(configureRequestedSpy->count(), 5); // The rule should be applied again if the client appears after it's been closed. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeRemember) void TestXdgShellClientRules::testSizeRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The initial configure event should contain size hint set by the rule. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // One should still be able to resize the client. QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); QSignalSpy surfaceSizeChangedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); QVERIFY(surfaceSizeChangedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(!client->isMove()); QVERIFY(client->isResize()); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); const QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 4); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(surfaceSizeChangedSpy.count(), 1); QCOMPARE(surfaceSizeChangedSpy.last().first().toSize(), QSize(488, 640)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 0); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(488, 640), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(488, 640)); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); - QEXPECT_FAIL("", "Interactive resize is not spec-compliant", Continue); QVERIFY(configureRequestedSpy->wait(10)); - QEXPECT_FAIL("", "Interactive resize is not spec-compliant", Continue); QCOMPARE(configureRequestedSpy->count(), 5); // If the client appears again, it should have the last known size. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(488, 640)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(488, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(488, 640)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeForce) void TestXdgShellClientRules::testSizeForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The initial configure event should contain size hint set by the rule. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Any attempt to resize the client should not succeed. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(clientStartMoveResizedSpy.count(), 0); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QVERIFY(!configureRequestedSpy->wait(100)); // If the client appears again, the size should still be forced. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeApplyNow) void TestXdgShellClientRules::testSizeApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The expected surface dimensions should be set by the rule. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(0, 0)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(100, 50)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The compositor should send a configure event with a new size. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); // Draw the surface with the new size. QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(480, 640)); QVERIFY(!configureRequestedSpy->wait(100)); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!configureRequestedSpy->wait(100)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSizeForceTemporarily) void TestXdgShellClientRules::testSizeForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("size", QSize(480, 640)); group.writeEntry("sizerule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // The initial configure event should contain size hint set by the rule. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(480, 640)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(480, 640), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isResizable()); QCOMPARE(client->size(), QSize(480, 640)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Any attempt to resize the client should not succeed. QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QCOMPARE(workspace()->moveResizeClient(), nullptr); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), nullptr); QCOMPARE(clientStartMoveResizedSpy.count(), 0); QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); QVERIFY(!configureRequestedSpy->wait(100)); // The rule should be discarded when the client is closed. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().first().toSize(), QSize(0, 0)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isResizable()); QCOMPARE(client->size(), QSize(100, 50)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMaximizeDontAffect) void TestXdgShellClientRules::testMaximizeDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("maximizehoriz", true); group.writeEntry("maximizehorizrule", int(Rules::DontAffect)); group.writeEntry("maximizevert", true); group.writeEntry("maximizevertrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->size(), QSize(100, 50)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMaximizeApply) void TestXdgShellClientRules::testMaximizeApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("maximizehoriz", true); group.writeEntry("maximizehorizrule", int(Rules::Apply)); group.writeEntry("maximizevert", true); group.writeEntry("maximizevertrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->size(), QSize(1280, 1024)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // One should still be able to change the maximized state of the client. workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); + QEXPECT_FAIL("", "Geometry restore is set to the first valid geometry", Continue); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(100, 50)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); // If we create the client again, it should be initially maximized. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->size(), QSize(1280, 1024)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMaximizeRemember) void TestXdgShellClientRules::testMaximizeRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("maximizehoriz", true); group.writeEntry("maximizehorizrule", int(Rules::Remember)); group.writeEntry("maximizevert", true); group.writeEntry("maximizevertrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->size(), QSize(1280, 1024)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // One should still be able to change the maximized state of the client. workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); + QEXPECT_FAIL("", "Geometry restore is set to the first valid geometry", Continue); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(100, 50)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); // If we create the client again, it should not be maximized (because last time it wasn't). shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->size(), QSize(100, 50)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMaximizeForce) void TestXdgShellClientRules::testMaximizeForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("maximizehoriz", true); group.writeEntry("maximizehorizrule", int(Rules::Force)); group.writeEntry("maximizevert", true); group.writeEntry("maximizevertrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->size(), QSize(1280, 1024)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Any attempt to change the maximized state should not succeed. const QRect oldGeometry = client->frameGeometry(); workspace()->slotWindowMaximize(); QVERIFY(!configureRequestedSpy->wait(100)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->frameGeometry(), oldGeometry); // If we create the client again, the maximized state should still be forced. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->size(), QSize(1280, 1024)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMaximizeApplyNow) void TestXdgShellClientRules::testMaximizeApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->size(), QSize(100, 50)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("maximizehoriz", true); group.writeEntry("maximizehorizrule", int(Rules::ApplyNow)); group.writeEntry("maximizevert", true); group.writeEntry("maximizevertrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We should receive a configure event with a new surface size. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Draw contents of the maximized client. QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(1280, 1024)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); // The client still has to be maximizeable. QVERIFY(client->isMaximizable()); // Restore the client. workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 4); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(100, 50)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); Test::render(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->size(), QSize(100, 50)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); // The rule should be discarded after it's been applied. const QRect oldGeometry = client->frameGeometry(); client->evaluateWindowRules(); QVERIFY(!configureRequestedSpy->wait(100)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->frameGeometry(), oldGeometry); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMaximizeForceTemporarily) void TestXdgShellClientRules::testMaximizeForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("maximizehoriz", true); group.writeEntry("maximizehorizrule", int(Rules::ForceTemporarily)); group.writeEntry("maximizevert", true); group.writeEntry("maximizevertrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer surface; surface.reset(Test::createSurface()); QScopedPointer shellSurface; shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QScopedPointer configureRequestedSpy; configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); // Wait for the initial configure event. XdgShellSurface::States states; QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(1280, 1024)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Map the client. shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(!client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->size(), QSize(1280, 1024)); // We should receive a configure event when the client becomes active. QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); // Any attempt to change the maximized state should not succeed. const QRect oldGeometry = client->frameGeometry(); workspace()->slotWindowMaximize(); QVERIFY(!configureRequestedSpy->wait(100)); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeFull); QCOMPARE(client->frameGeometry(), oldGeometry); // The rule should be discarded if we close the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); surface.reset(Test::createSurface()); shellSurface.reset(createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); configureRequestedSpy.reset(new QSignalSpy(shellSurface.data(), &XdgShellSurface::configureRequested)); shellSurface->setAppId("org.kde.foo"); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 1); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->ackConfigure(configureRequestedSpy->last().at(2).value()); client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(client); QVERIFY(client->isActive()); QVERIFY(client->isMaximizable()); QCOMPARE(client->maximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->requestedMaximizeMode(), MaximizeMode::MaximizeRestore); QCOMPARE(client->size(), QSize(100, 50)); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 2); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); // Destroy the client. shellSurface.reset(); surface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopDontAffect) void TestXdgShellClientRules::testDesktopDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should appear on the current virtual desktop. QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopApply) void TestXdgShellClientRules::testDesktopApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should appear on the second virtual desktop. QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // We still should be able to move the client between desktops. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // If we re-open the client, it should appear on the second virtual desktop again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopRemember) void TestXdgShellClientRules::testDesktopRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Move the client to the first virtual desktop. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // If we create the client again, it should appear on the first virtual desktop. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopForce) void TestXdgShellClientRules::testDesktopForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should appear on the second virtual desktop. QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Any attempt to move the client to another virtual desktop should fail. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // If we re-open the client, it should appear on the second virtual desktop again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopApplyNow) void TestXdgShellClientRules::testDesktopApplyNow() { // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should have been moved to the second virtual desktop. QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // One should still be able to move the client between desktops. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // The rule should not be applied again. client->evaluateWindowRules(); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopForceTemporarily) void TestXdgShellClientRules::testDesktopForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("desktop", 2); group.writeEntry("desktoprule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // We need at least two virtual desktop for this test. VirtualDesktopManager::self()->setCount(2); QCOMPARE(VirtualDesktopManager::self()->count(), 2u); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should appear on the second virtual desktop. QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // Any attempt to move the client to another virtual desktop should fail. workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 2); // The rule should be discarded when the client is withdrawn. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); VirtualDesktopManager::self()->setCurrent(1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // One should be able to move the client between desktops. workspace()->sendClientToDesktop(client, 2, true); QCOMPARE(client->desktop(), 2); QCOMPARE(VirtualDesktopManager::self()->current(), 1); workspace()->sendClientToDesktop(client, 1, true); QCOMPARE(client->desktop(), 1); QCOMPARE(VirtualDesktopManager::self()->current(), 1); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeDontAffect) void TestXdgShellClientRules::testMinimizeDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", true); group.writeEntry("minimizerule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); // The client should not be minimized. QVERIFY(!client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeApply) void TestXdgShellClientRules::testMinimizeApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", true); group.writeEntry("minimizerule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); // The client should be minimized. QVERIFY(client->isMinimized()); // We should still be able to unminimize the client. client->unminimize(); QVERIFY(!client->isMinimized()); // If we re-open the client, it should be minimized back again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeRemember) void TestXdgShellClientRules::testMinimizeRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", false); group.writeEntry("minimizerule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(!client->isMinimized()); // Minimize the client. client->minimize(); QVERIFY(client->isMinimized()); // If we open the client again, it should be minimized. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeForce) void TestXdgShellClientRules::testMinimizeForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", false); group.writeEntry("minimizerule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->isMinimizable()); QVERIFY(!client->isMinimized()); // Any attempt to minimize the client should fail. client->minimize(); QVERIFY(!client->isMinimized()); // If we re-open the client, the minimized state should still be forced. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->isMinimizable()); QVERIFY(!client->isMinimized()); client->minimize(); QVERIFY(!client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeApplyNow) void TestXdgShellClientRules::testMinimizeApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(!client->isMinimized()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", true); group.writeEntry("minimizerule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should be minimized now. QVERIFY(client->isMinimizable()); QVERIFY(client->isMinimized()); // One is still able to unminimize the client. client->unminimize(); QVERIFY(!client->isMinimized()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(client->isMinimizable()); QVERIFY(!client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testMinimizeForceTemporarily) void TestXdgShellClientRules::testMinimizeForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("minimize", false); group.writeEntry("minimizerule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->isMinimizable()); QVERIFY(!client->isMinimized()); // Any attempt to minimize the client should fail until the client is closed. client->minimize(); QVERIFY(!client->isMinimized()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isMinimizable()); QVERIFY(!client->isMinimized()); client->minimize(); QVERIFY(client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarDontAffect) void TestXdgShellClientRules::testSkipTaskbarDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be affected by the rule. QVERIFY(!client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarApply) void TestXdgShellClientRules::testSkipTaskbarApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a taskbar. QVERIFY(client->skipTaskbar()); // Though one can change that. client->setOriginalSkipTaskbar(false); QVERIFY(!client->skipTaskbar()); // Reopen the client, the rule should be applied again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarRemember) void TestXdgShellClientRules::testSkipTaskbarRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a taskbar. QVERIFY(client->skipTaskbar()); // Change the skip-taskbar state. client->setOriginalSkipTaskbar(false); QVERIFY(!client->skipTaskbar()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be included on a taskbar. QVERIFY(!client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarForce) void TestXdgShellClientRules::testSkipTaskbarForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a taskbar. QVERIFY(client->skipTaskbar()); // Any attempt to change the skip-taskbar state should not succeed. client->setOriginalSkipTaskbar(false); QVERIFY(client->skipTaskbar()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The skip-taskbar state should be still forced. QVERIFY(client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarApplyNow) void TestXdgShellClientRules::testSkipTaskbarApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipTaskbar()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should not be on a taskbar now. QVERIFY(client->skipTaskbar()); // Also, one change the skip-taskbar state. client->setOriginalSkipTaskbar(false); QVERIFY(!client->skipTaskbar()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipTaskbarForceTemporarily) void TestXdgShellClientRules::testSkipTaskbarForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skiptaskbar", true); group.writeEntry("skiptaskbarrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a taskbar. QVERIFY(client->skipTaskbar()); // Any attempt to change the skip-taskbar state should not succeed. client->setOriginalSkipTaskbar(false); QVERIFY(client->skipTaskbar()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipTaskbar()); // The skip-taskbar state is no longer forced. client->setOriginalSkipTaskbar(true); QVERIFY(client->skipTaskbar()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerDontAffect) void TestXdgShellClientRules::testSkipPagerDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be affected by the rule. QVERIFY(!client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerApply) void TestXdgShellClientRules::testSkipPagerApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a pager. QVERIFY(client->skipPager()); // Though one can change that. client->setSkipPager(false); QVERIFY(!client->skipPager()); // Reopen the client, the rule should be applied again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerRemember) void TestXdgShellClientRules::testSkipPagerRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a pager. QVERIFY(client->skipPager()); // Change the skip-pager state. client->setSkipPager(false); QVERIFY(!client->skipPager()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be included on a pager. QVERIFY(!client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerForce) void TestXdgShellClientRules::testSkipPagerForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a pager. QVERIFY(client->skipPager()); // Any attempt to change the skip-pager state should not succeed. client->setSkipPager(false); QVERIFY(client->skipPager()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The skip-pager state should be still forced. QVERIFY(client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerApplyNow) void TestXdgShellClientRules::testSkipPagerApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipPager()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should not be on a pager now. QVERIFY(client->skipPager()); // Also, one change the skip-pager state. client->setSkipPager(false); QVERIFY(!client->skipPager()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipPagerForceTemporarily) void TestXdgShellClientRules::testSkipPagerForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skippager", true); group.writeEntry("skippagerrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be included on a pager. QVERIFY(client->skipPager()); // Any attempt to change the skip-pager state should not succeed. client->setSkipPager(false); QVERIFY(client->skipPager()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipPager()); // The skip-pager state is no longer forced. client->setSkipPager(true); QVERIFY(client->skipPager()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherDontAffect) void TestXdgShellClientRules::testSkipSwitcherDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should not be affected by the rule. QVERIFY(!client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherApply) void TestXdgShellClientRules::testSkipSwitcherApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be excluded from window switching effects. QVERIFY(client->skipSwitcher()); // Though one can change that. client->setSkipSwitcher(false); QVERIFY(!client->skipSwitcher()); // Reopen the client, the rule should be applied again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherRemember) void TestXdgShellClientRules::testSkipSwitcherRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be excluded from window switching effects. QVERIFY(client->skipSwitcher()); // Change the skip-switcher state. client->setSkipSwitcher(false); QVERIFY(!client->skipSwitcher()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be included in window switching effects. QVERIFY(!client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherForce) void TestXdgShellClientRules::testSkipSwitcherForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be excluded from window switching effects. QVERIFY(client->skipSwitcher()); // Any attempt to change the skip-switcher state should not succeed. client->setSkipSwitcher(false); QVERIFY(client->skipSwitcher()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The skip-switcher state should be still forced. QVERIFY(client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherApplyNow) void TestXdgShellClientRules::testSkipSwitcherApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipSwitcher()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should be excluded from window switching effects now. QVERIFY(client->skipSwitcher()); // Also, one change the skip-switcher state. client->setSkipSwitcher(false); QVERIFY(!client->skipSwitcher()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testSkipSwitcherForceTemporarily) void TestXdgShellClientRules::testSkipSwitcherForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("skipswitcher", true); group.writeEntry("skipswitcherrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The client should be excluded from window switching effects. QVERIFY(client->skipSwitcher()); // Any attempt to change the skip-switcher state should not succeed. client->setSkipSwitcher(false); QVERIFY(client->skipSwitcher()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->skipSwitcher()); // The skip-switcher state is no longer forced. client->setSkipSwitcher(true); QVERIFY(client->skipSwitcher()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveDontAffect) void TestXdgShellClientRules::testKeepAboveDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The keep-above state of the client should not be affected by the rule. QVERIFY(!client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveApply) void TestXdgShellClientRules::testKeepAboveApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept above. QVERIFY(client->keepAbove()); // One should also be able to alter the keep-above state. client->setKeepAbove(false); QVERIFY(!client->keepAbove()); // If one re-opens the client, it should be kept above back again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveRemember) void TestXdgShellClientRules::testKeepAboveRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept above. QVERIFY(client->keepAbove()); // Unset the keep-above state. client->setKeepAbove(false); QVERIFY(!client->keepAbove()); delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); // Re-open the client, it should not be kept above. std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveForce) void TestXdgShellClientRules::testKeepAboveForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept above. QVERIFY(client->keepAbove()); // Any attemt to unset the keep-above should not succeed. client->setKeepAbove(false); QVERIFY(client->keepAbove()); // If we re-open the client, it should still be kept above. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveApplyNow) void TestXdgShellClientRules::testKeepAboveApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepAbove()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should now be kept above other clients. QVERIFY(client->keepAbove()); // One is still able to change the keep-above state of the client. client->setKeepAbove(false); QVERIFY(!client->keepAbove()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepAboveForceTemporarily) void TestXdgShellClientRules::testKeepAboveForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept above. QVERIFY(client->keepAbove()); // Any attempt to alter the keep-above state should not succeed. client->setKeepAbove(false); QVERIFY(client->keepAbove()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepAbove()); // The keep-above state is no longer forced. client->setKeepAbove(true); QVERIFY(client->keepAbove()); client->setKeepAbove(false); QVERIFY(!client->keepAbove()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowDontAffect) void TestXdgShellClientRules::testKeepBelowDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The keep-below state of the client should not be affected by the rule. QVERIFY(!client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowApply) void TestXdgShellClientRules::testKeepBelowApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept below. QVERIFY(client->keepBelow()); // One should also be able to alter the keep-below state. client->setKeepBelow(false); QVERIFY(!client->keepBelow()); // If one re-opens the client, it should be kept above back again. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowRemember) void TestXdgShellClientRules::testKeepBelowRemember() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept below. QVERIFY(client->keepBelow()); // Unset the keep-below state. client->setKeepBelow(false); QVERIFY(!client->keepBelow()); delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); // Re-open the client, it should not be kept below. std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowForce) void TestXdgShellClientRules::testKeepBelowForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept below. QVERIFY(client->keepBelow()); // Any attemt to unset the keep-below should not succeed. client->setKeepBelow(false); QVERIFY(client->keepBelow()); // If we re-open the client, it should still be kept below. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowApplyNow) void TestXdgShellClientRules::testKeepBelowApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepBelow()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should now be kept below other clients. QVERIFY(client->keepBelow()); // One is still able to change the keep-below state of the client. client->setKeepBelow(false); QVERIFY(!client->keepBelow()); // The rule should not be applied again. client->evaluateWindowRules(); QVERIFY(!client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testKeepBelowForceTemporarily) void TestXdgShellClientRules::testKeepBelowForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("below", true); group.writeEntry("belowrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // Initially, the client should be kept below. QVERIFY(client->keepBelow()); // Any attempt to alter the keep-below state should not succeed. client->setKeepBelow(false); QVERIFY(client->keepBelow()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(!client->keepBelow()); // The keep-below state is no longer forced. client->setKeepBelow(true); QVERIFY(client->keepBelow()); client->setKeepBelow(false); QVERIFY(!client->keepBelow()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutDontAffect) void TestXdgShellClientRules::testShortcutDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QCOMPARE(client->shortcut(), QKeySequence()); client->minimize(); QVERIFY(client->isMinimized()); // If we press the window shortcut, nothing should happen. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(!clientUnminimizedSpy.wait(100)); QVERIFY(client->isMinimized()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutApply) void TestXdgShellClientRules::testShortcutApply() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::Apply)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // If we press the window shortcut, the window should be brought back to user. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // One can also change the shortcut. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // The old shortcut should do nothing. client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(!clientUnminimizedSpy.wait(100)); QVERIFY(client->isMinimized()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The window shortcut should be set back to Ctrl+Alt+1. QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutRemember) void TestXdgShellClientRules::testShortcutRemember() { QSKIP("KWin core doesn't try to save the last used window shortcut"); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::Remember)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // If we press the window shortcut, the window should be brought back to user. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Change the window shortcut to Ctrl+Alt+2. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The window shortcut should be set to the last known value. QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutForce) void TestXdgShellClientRules::testShortcutForce() { QSKIP("KWin core can't release forced window shortcuts"); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // If we press the window shortcut, the window should be brought back to user. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Any attempt to change the window shortcut should not succeed. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(!clientUnminimizedSpy.wait(100)); QVERIFY(client->isMinimized()); // Reopen the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // The window shortcut should still be forced. QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutApplyNow) void TestXdgShellClientRules::testShortcutApplyNow() { // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->shortcut().isEmpty()); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::ApplyNow)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // The client should now have a window shortcut assigned. QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Assign a different shortcut. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // The rule should not be applied again. client->evaluateWindowRules(); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_2})); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testShortcutForceTemporarily) void TestXdgShellClientRules::testShortcutForceTemporarily() { QSKIP("KWin core can't release forced window shortcuts"); // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("shortcut", "Ctrl+Alt+1"); group.writeEntry("shortcutrule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); // If we press the window shortcut, the window should be brought back to user. QSignalSpy clientUnminimizedSpy(client, &AbstractClient::clientUnminimized); QVERIFY(clientUnminimizedSpy.isValid()); quint32 timestamp = 1; QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_1, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(clientUnminimizedSpy.wait()); QVERIFY(!client->isMinimized()); // Any attempt to change the window shortcut should not succeed. client->setShortcut(QStringLiteral("Ctrl+Alt+2")); QCOMPARE(client->shortcut(), (QKeySequence{Qt::CTRL + Qt::ALT + Qt::Key_1})); client->minimize(); QVERIFY(client->isMinimized()); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTCTRL, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyPressed(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_2, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTALT, timestamp++); kwinApp()->platform()->keyboardKeyReleased(KEY_LEFTCTRL, timestamp++); QVERIFY(!clientUnminimizedSpy.wait(100)); QVERIFY(client->isMinimized()); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->shortcut().isEmpty()); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testDesktopFileDontAffect) void TestXdgShellClientRules::testDesktopFileDontAffect() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileApply) void TestXdgShellClientRules::testDesktopFileApply() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileRemember) void TestXdgShellClientRules::testDesktopFileRemember() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileForce) void TestXdgShellClientRules::testDesktopFileForce() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileApplyNow) void TestXdgShellClientRules::testDesktopFileApplyNow() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testDesktopFileForceTemporarily) void TestXdgShellClientRules::testDesktopFileForceTemporarily() { // Currently, the desktop file name is derived from the app id. If the app id is // changed, then the old rules will be lost. Either setDesktopFileName should // be exposed or the desktop file name rule should be removed for wayland clients. QSKIP("Needs changes in KWin core to pass"); } TEST_DATA(testActiveOpacityDontAffect) void TestXdgShellClientRules::testActiveOpacityDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityactive", 90); group.writeEntry("opacityactiverule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // The opacity should not be affected by the rule. QCOMPARE(client->opacity(), 1.0); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testActiveOpacityForce) void TestXdgShellClientRules::testActiveOpacityForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityactive", 90); group.writeEntry("opacityactiverule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 0.9); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testActiveOpacityForceTemporarily) void TestXdgShellClientRules::testActiveOpacityForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityactive", 90); group.writeEntry("opacityactiverule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 0.9); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 1.0); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testInactiveOpacityDontAffect) void TestXdgShellClientRules::testInactiveOpacityDontAffect() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityinactive", 80); group.writeEntry("opacityinactiverule", int(Rules::DontAffect)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); // Make the client inactive. workspace()->setActiveClient(nullptr); QVERIFY(!client->isActive()); // The opacity of the client should not be affected by the rule. QCOMPARE(client->opacity(), 1.0); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testInactiveOpacityForce) void TestXdgShellClientRules::testInactiveOpacityForce() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityinactive", 80); group.writeEntry("opacityinactiverule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 1.0); // Make the client inactive. workspace()->setActiveClient(nullptr); QVERIFY(!client->isActive()); // The opacity should be forced by the rule. QCOMPARE(client->opacity(), 0.8); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } TEST_DATA(testInactiveOpacityForceTemporarily) void TestXdgShellClientRules::testInactiveOpacityForceTemporarily() { // Initialize RuleBook with the test rule. auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("opacityinactive", 80); group.writeEntry("opacityinactiverule", int(Rules::ForceTemporarily)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); // Create the test client. QFETCH(Test::XdgShellSurfaceType, type); AbstractClient *client; Surface *surface; XdgShellSurface *shellSurface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 1.0); // Make the client inactive. workspace()->setActiveClient(nullptr); QVERIFY(!client->isActive()); // The opacity should be forced by the rule. QCOMPARE(client->opacity(), 0.8); // The rule should be discarded when the client is closed. delete shellSurface; delete surface; std::tie(client, surface, shellSurface) = createWindow(type, "org.kde.foo"); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->opacity(), 1.0); workspace()->setActiveClient(nullptr); QVERIFY(!client->isActive()); QCOMPARE(client->opacity(), 1.0); // Destroy the client. delete shellSurface; delete surface; QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClientRules::testMatchAfterNameChange() { KSharedConfig::Ptr config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); config->group("General").writeEntry("count", 1); KConfigGroup group = config->group("1"); group.writeEntry("above", true); group.writeEntry("aboverule", int(Rules::Force)); group.writeEntry("wmclass", "org.kde.foo"); group.writeEntry("wmclasscomplete", false); group.writeEntry("wmclassmatch", int(Rules::ExactMatch)); group.sync(); RuleBook::self()->setConfig(config); workspace()->slotReconfigure(); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QCOMPARE(c->keepAbove(), false); QSignalSpy desktopFileNameSpy(c, &AbstractClient::desktopFileNameChanged); QVERIFY(desktopFileNameSpy.isValid()); shellSurface->setAppId(QByteArrayLiteral("org.kde.foo")); QVERIFY(desktopFileNameSpy.wait()); QCOMPARE(c->keepAbove(), true); } WAYLANDTEST_MAIN(TestXdgShellClientRules) #include "xdgshellclient_rules_test.moc" diff --git a/autotests/integration/xdgshellclient_test.cpp b/autotests/integration/xdgshellclient_test.cpp index 4cf7bfab4..821f1a64d 100644 --- a/autotests/integration/xdgshellclient_test.cpp +++ b/autotests/integration/xdgshellclient_test.cpp @@ -1,1625 +1,1666 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2016 Martin Gräßlin Copyright (C) 2019 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 "kwin_wayland_test.h" #include "abstract_client.h" #include "cursor.h" #include "decorations/decorationbridge.h" #include "decorations/settings.h" #include "effects.h" #include "deleted.h" #include "platform.h" #include "screens.h" #include "wayland_server.h" #include "workspace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // system #include #include #include #include using namespace KWin; using namespace KWayland::Client; static const QString s_socketName = QStringLiteral("wayland_test_kwin_xdgshellclient-0"); class TestXdgShellClient : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void testMapUnmapMap_data(); void testMapUnmapMap(); void testDesktopPresenceChanged(); void testTransientPositionAfterRemap(); void testWindowOutputs_data(); void testWindowOutputs(); void testMinimizeActiveWindow_data(); void testMinimizeActiveWindow(); void testFullscreen_data(); void testFullscreen(); void testFullscreenRestore_data(); void testFullscreenRestore(); void testUserCanSetFullscreen_data(); void testUserCanSetFullscreen(); void testUserSetFullscreen_data(); void testUserSetFullscreen(); 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 testSendClientWithTransientToDesktop_data(); void testSendClientWithTransientToDesktop(); void testMinimizeWindowWithTransients_data(); void testMinimizeWindowWithTransients(); void testXdgDecoration_data(); void testXdgDecoration(); void testXdgNeverCommitted(); void testXdgInitialState(); void testXdgInitiallyMaximised(); void testXdgInitiallyFullscreen(); void testXdgInitiallyMinimized(); void testXdgWindowGeometryIsntSet(); void testXdgWindowGeometryAttachBuffer(); void testXdgWindowGeometryAttachSubSurface(); void testXdgWindowGeometryInteractiveResize(); void testXdgWindowGeometryFullScreen(); void testXdgWindowGeometryMaximize(); }; void TestXdgShellClient::initTestCase() { qRegisterMetaType(); qRegisterMetaType(); qRegisterMetaType(); QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); QVERIFY(workspaceCreatedSpy.isValid()); kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); QMetaObject::invokeMethod(kwinApp()->platform(), "setVirtualOutputs", Qt::DirectConnection, Q_ARG(int, 2)); kwinApp()->start(); QVERIFY(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 TestXdgShellClient::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Decoration | Test::AdditionalWaylandInterface::XdgDecoration | Test::AdditionalWaylandInterface::AppMenu)); screens()->setCurrent(0); KWin::Cursors::self()->mouse()->setPos(QPoint(1280, 512)); } void TestXdgShellClient::cleanup() { Test::destroyWaylandConnection(); } void TestXdgShellClient::testMapUnmapMap_data() { QTest::addColumn("type"); QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::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::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(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()); QCOMPARE(client->isInternal(), false); QVERIFY(client->effectWindow()); QVERIFY(!client->effectWindow()->internalWindow()); QCOMPARE(client->internalId().isNull(), false); const auto uuid = client->internalId(); QUuid deletedUuid; QCOMPARE(deletedUuid.isNull(), true); connect(client, &AbstractClient::windowClosed, this, [&deletedUuid] (Toplevel *, Deleted *d) { deletedUuid = d->internalId(); }); // now unmap QSignalSpy hiddenSpy(client, &AbstractClient::windowHidden); QVERIFY(hiddenSpy.isValid()); QSignalSpy windowClosedSpy(client, &AbstractClient::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, &AbstractClient::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); QCOMPARE(client->internalId(), uuid); 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); QCOMPARE(deletedUuid.isNull(), false); QCOMPARE(deletedUuid, uuid); } void TestXdgShellClient::testDesktopPresenceChanged() { // this test verifies that the desktop presence changed signals are properly emitted QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->desktop(), 1); effects->setNumberOfDesktops(4); QSignalSpy desktopPresenceChangedClientSpy(c, &AbstractClient::desktopPresenceChanged); QVERIFY(desktopPresenceChangedClientSpy.isValid()); QSignalSpy desktopPresenceChangedWorkspaceSpy(workspace(), &Workspace::desktopPresenceChanged); QVERIFY(desktopPresenceChangedWorkspaceSpy.isValid()); QSignalSpy desktopPresenceChangedEffectsSpy(effects, &EffectsHandler::desktopPresenceChanged); QVERIFY(desktopPresenceChangedEffectsSpy.isValid()); // let's change the desktop workspace()->sendClientToDesktop(c, 2, false); QCOMPARE(c->desktop(), 2); QCOMPARE(desktopPresenceChangedClientSpy.count(), 1); QCOMPARE(desktopPresenceChangedWorkspaceSpy.count(), 1); QCOMPARE(desktopPresenceChangedEffectsSpy.count(), 1); // verify the arguments QCOMPARE(desktopPresenceChangedClientSpy.first().at(0).value(), c); QCOMPARE(desktopPresenceChangedClientSpy.first().at(1).toInt(), 1); QCOMPARE(desktopPresenceChangedWorkspaceSpy.first().at(0).value(), c); QCOMPARE(desktopPresenceChangedWorkspaceSpy.first().at(1).toInt(), 1); QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(0).value(), c->effectWindow()); QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(1).toInt(), 1); QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(2).toInt(), 2); } void TestXdgShellClient::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::createXdgShellStableSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); // create the Transient window XdgPositioner positioner(QSize(50, 40), QRect(0, 0, 5, 10)); positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); positioner.setGravity(Qt::BottomEdge | Qt::RightEdge); QScopedPointer transientSurface(Test::createSurface()); QScopedPointer transientShellSurface(Test::createXdgShellStablePopup(transientSurface.data(), shellSurface.data(), positioner)); auto transient = Test::renderAndWaitForShown(transientSurface.data(), positioner.initialSize(), Qt::blue); QVERIFY(transient); QCOMPARE(transient->frameGeometry(), QRect(c->frameGeometry().topLeft() + QPoint(5, 10), QSize(50, 40))); // unmap the transient QSignalSpy windowHiddenSpy(transient, &AbstractClient::windowHidden); QVERIFY(windowHiddenSpy.isValid()); transientSurface->attachBuffer(Buffer::Ptr()); transientSurface->commit(Surface::CommitFlag::None); QVERIFY(windowHiddenSpy.wait()); // now move the parent surface c->setFrameGeometry(c->frameGeometry().translated(5, 10)); // now map the transient again QSignalSpy windowShownSpy(transient, &AbstractClient::windowShown); QVERIFY(windowShownSpy.isValid()); Test::render(transientSurface.data(), QSize(50, 40), Qt::blue); QVERIFY(windowShownSpy.wait()); QCOMPARE(transient->frameGeometry(), QRect(c->frameGeometry().topLeft() + QPoint(5, 10), QSize(50, 40))); } void TestXdgShellClient::testWindowOutputs_data() { QTest::addColumn("type"); QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testWindowOutputs() { QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(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->setFrameGeometry(QRect(QPoint(100,100), size)); //we don't don't know where the compositor first placed this window, //this might fire, it might not outputEnteredSpy.wait(5); outputEnteredSpy.clear(); QCOMPARE(surface->outputs().count(), 1); QCOMPARE(surface->outputs().first()->globalPosition(), QPoint(0,0)); //move to overlapping both first and second screen c->setFrameGeometry(QRect(QPoint(1250,100), size)); QVERIFY(outputEnteredSpy.wait()); QCOMPARE(outputEnteredSpy.count(), 1); QCOMPARE(outputLeftSpy.count(), 0); QCOMPARE(surface->outputs().count(), 2); QVERIFY(surface->outputs()[0] != surface->outputs()[1]); //move entirely into second screen c->setFrameGeometry(QRect(QPoint(1400,100), size)); QVERIFY(outputLeftSpy.wait()); QCOMPARE(outputEnteredSpy.count(), 1); QCOMPARE(outputLeftSpy.count(), 1); QCOMPARE(surface->outputs().count(), 1); QCOMPARE(surface->outputs().first()->globalPosition(), QPoint(1280,0)); } void TestXdgShellClient::testMinimizeActiveWindow_data() { QTest::addColumn("type"); QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testMinimizeActiveWindow() { // this test verifies that when minimizing the active window it gets deactivated QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(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 TestXdgShellClient::testFullscreen_data() { QTest::addColumn("type"); QTest::addColumn("decoMode"); QTest::newRow("xdgShellWmBase") << Test::XdgShellSurfaceType::XdgShellStable << ServerSideDecoration::Mode::Client; QTest::newRow("xdgShellWmBase - deco") << Test::XdgShellSurfaceType::XdgShellStable << ServerSideDecoration::Mode::Server; } void TestXdgShellClient::testFullscreen() { // this test verifies that a window can be properly fullscreened + + XdgShellSurface::States states; + QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); QVERIFY(shellSurface); // create deco QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); QSignalSpy decoSpy(deco.data(), &ServerSideDecoration::modeChanged); QVERIFY(decoSpy.isValid()); QVERIFY(decoSpy.wait()); QFETCH(ServerSideDecoration::Mode, decoMode); deco->requestMode(decoMode); QVERIFY(decoSpy.wait()); QCOMPARE(deco->mode(), decoMode); - auto 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->clientSizeToFrameSize(c->clientSize()), c->frameGeometry().size()); - QSignalSpy fullscreenChangedSpy(c, &AbstractClient::fullScreenChanged); - QVERIFY(fullscreenChangedSpy.isValid()); - QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->layer(), NormalLayer); + QVERIFY(!client->isFullScreen()); + QCOMPARE(client->clientSize(), QSize(100, 50)); + QCOMPARE(client->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QCOMPARE(client->clientSizeToFrameSize(client->clientSize()), client->size()); + + QSignalSpy fullScreenChangedSpy(client, &AbstractClient::fullScreenChanged); + QVERIFY(fullScreenChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); - QSignalSpy sizeChangeRequestedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); - QVERIFY(sizeChangeRequestedSpy.isValid()); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); + // Wait for the compositor to send a configure event with the Activated state. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Activated); + + // Ask the compositor to show the window in full screen mode. shellSurface->setFullscreen(true); - QVERIFY(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(frameGeometryChangedSpy.isEmpty()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Fullscreen); + QCOMPARE(configureRequestedSpy.last().at(0).value(), screens()->size(0)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); - Test::render(surface.data(), sizeChangeRequestedSpy.first().first().toSize(), Qt::red); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::red); + +#if 0 // TODO: Uncomment when full screen state updates are truly asynchronous. + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 1); +#else QVERIFY(frameGeometryChangedSpy.wait()); - QCOMPARE(frameGeometryChangedSpy.count(), 1); - QVERIFY(c->isFullScreen()); - QVERIFY(!c->isDecorated()); - QCOMPARE(c->frameGeometry(), QRect(QPoint(0, 0), sizeChangeRequestedSpy.first().first().toSize())); - QCOMPARE(c->layer(), ActiveLayer); + QCOMPARE(fullScreenChangedSpy.count(), 1); +#endif + QVERIFY(client->isFullScreen()); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->layer(), ActiveLayer); + QCOMPARE(client->frameGeometry(), QRect(QPoint(0, 0), screens()->size(0))); - // swap back to normal + // Ask the compositor to show the window in normal mode. shellSurface->setFullscreen(false); - 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); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!(states & XdgShellSurface::State::Fullscreen)); + QCOMPARE(configureRequestedSpy.last().at(0).value(), QSize(100, 50)); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::blue); + +#if 0 // TODO: Uncomment when full screen state updates are truly asynchronous. + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 2); +#else + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 2); +#endif + QCOMPARE(client->clientSize(), QSize(100, 50)); + QVERIFY(!client->isFullScreen()); + QCOMPARE(client->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QCOMPARE(client->layer(), NormalLayer); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClient::testFullscreenRestore_data() { QTest::addColumn("type"); QTest::newRow("xdgShellWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testFullscreenRestore() { // this test verifies that windows created fullscreen can be later properly restored QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); XdgShellSurface *xdgShellSurface = Test::createXdgShellSurface(type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly); QSignalSpy configureRequestedSpy(xdgShellSurface, &XdgShellSurface::configureRequested); // fullscreen the window xdgShellSurface->setFullscreen(true); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); QCOMPARE(configureRequestedSpy.count(), 1); const auto size = configureRequestedSpy.first()[0].value(); const auto state = configureRequestedSpy.first()[1].value(); QCOMPARE(size, screens()->size(0)); QVERIFY(state & KWayland::Client::XdgShellSurface::State::Fullscreen); xdgShellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); QVERIFY(c); QVERIFY(c->isFullScreen()); configureRequestedSpy.wait(100); QSignalSpy fullscreenChangedSpy(c, &AbstractClient::fullScreenChanged); QVERIFY(fullscreenChangedSpy.isValid()); QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); // swap back to normal configureRequestedSpy.clear(); xdgShellSurface->setFullscreen(false); QVERIFY(fullscreenChangedSpy.wait()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.last().first().toSize(), QSize(0, 0)); QVERIFY(!c->isFullScreen()); for (const auto &it: configureRequestedSpy) { xdgShellSurface->ackConfigure(it[2].toUInt()); } Test::render(surface.data(), QSize(100, 50), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 1); QVERIFY(!c->isFullScreen()); QCOMPARE(c->frameGeometry().size(), QSize(100, 50)); } void TestXdgShellClient::testUserCanSetFullscreen_data() { QTest::addColumn("type"); QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testUserCanSetFullscreen() { QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QVERIFY(!c->isFullScreen()); QVERIFY(c->userCanSetFullScreen()); } void TestXdgShellClient::testUserSetFullscreen_data() { QTest::addColumn("type"); QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testUserSetFullscreen() { QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface( type, surface.data(), surface.data(), Test::CreationSetup::CreateOnly)); QVERIFY(!shellSurface.isNull()); // wait for the initial configure event QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); surface->commit(Surface::CommitFlag::None); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); QVERIFY(!c->isFullScreen()); // The client gets activated, which gets another configure event. Though that's not relevant to the test configureRequestedSpy.wait(10); QSignalSpy fullscreenChangedSpy(c, &AbstractClient::fullScreenChanged); QVERIFY(fullscreenChangedSpy.isValid()); c->setFullScreen(true); QCOMPARE(c->isFullScreen(), true); configureRequestedSpy.clear(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QCOMPARE(configureRequestedSpy.first().at(0).toSize(), screens()->size(0)); const auto states = configureRequestedSpy.first().at(1).value(); QVERIFY(states.testFlag(KWayland::Client::XdgShellSurface::State::Fullscreen)); QVERIFY(states.testFlag(KWayland::Client::XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(KWayland::Client::XdgShellSurface::State::Maximized)); QVERIFY(!states.testFlag(KWayland::Client::XdgShellSurface::State::Resizing)); QCOMPARE(fullscreenChangedSpy.count(), 1); QVERIFY(c->isFullScreen()); shellSurface->ackConfigure(configureRequestedSpy.first().at(2).value()); // unset fullscreen again c->setFullScreen(false); QCOMPARE(c->isFullScreen(), false); configureRequestedSpy.clear(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QCOMPARE(configureRequestedSpy.first().at(0).toSize(), QSize(100, 50)); QVERIFY(!configureRequestedSpy.first().at(1).value().testFlag(KWayland::Client::XdgShellSurface::State::Fullscreen)); QCOMPARE(fullscreenChangedSpy.count(), 2); QVERIFY(!c->isFullScreen()); } void TestXdgShellClient::testMaximizedToFullscreen_data() { QTest::addColumn("type"); QTest::addColumn("decoMode"); QTest::newRow("xdgShellWmBase") << Test::XdgShellSurfaceType::XdgShellStable << ServerSideDecoration::Mode::Client; QTest::newRow("xdgShellWmBase - deco") << Test::XdgShellSurfaceType::XdgShellStable << ServerSideDecoration::Mode::Server; } void TestXdgShellClient::testMaximizedToFullscreen() { // this test verifies that a window can be properly fullscreened after maximizing + + XdgShellSurface::States states; + QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); QVERIFY(shellSurface); // create deco QScopedPointer deco(Test::waylandServerSideDecoration()->create(surface.data())); QSignalSpy decoSpy(deco.data(), &ServerSideDecoration::modeChanged); QVERIFY(decoSpy.isValid()); QVERIFY(decoSpy.wait()); QFETCH(ServerSideDecoration::Mode, decoMode); deco->requestMode(decoMode); QVERIFY(decoSpy.wait()); QCOMPARE(deco->mode(), decoMode); - auto 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, &AbstractClient::fullScreenChanged); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isFullScreen()); + QCOMPARE(client->clientSize(), QSize(100, 50)); + QCOMPARE(client->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + + QSignalSpy fullscreenChangedSpy(client, &AbstractClient::fullScreenChanged); QVERIFY(fullscreenChangedSpy.isValid()); - QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); - QSignalSpy sizeChangeRequestedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); - QVERIFY(sizeChangeRequestedSpy.isValid()); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); + + // Wait for the compositor to send a configure event with the Activated state. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Activated); + + // Ask the compositor to maximize the window. shellSurface->setMaximized(true); - QVERIFY(sizeChangeRequestedSpy.wait()); - QCOMPARE(sizeChangeRequestedSpy.count(), 1); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Maximized); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); - Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->maximizeMode(), MaximizeFull); - QCOMPARE(c->maximizeMode(), MaximizeFull); - QCOMPARE(frameGeometryChangedSpy.isEmpty(), false); - frameGeometryChangedSpy.clear(); - - // fullscreen the window + // Ask the compositor to show the window in full screen mode. shellSurface->setFullscreen(true); - 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()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + QCOMPARE(configureRequestedSpy.last().at(0).value(), screens()->size(0)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Maximized); + QVERIFY(states & XdgShellSurface::State::Fullscreen); - // render at the new size shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); - Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::red); - QVERIFY(c->isFullScreen()); - QVERIFY(!c->isDecorated()); - QCOMPARE(c->frameGeometry(), QRect(QPoint(0, 0), sizeChangeRequestedSpy.last().first().toSize())); - sizeChangeRequestedSpy.clear(); +#if 0 // TODO: Uncomment when full screen changes are truly asynchronous. + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 1); +#else + QTRY_COMPARE(fullscreenChangedSpy.count(), 1); +#endif + QCOMPARE(client->maximizeMode(), MaximizeFull); + QVERIFY(client->isFullScreen()); + QVERIFY(!client->isDecorated()); - // swap back to normal + // Switch back to normal mode. shellSurface->setFullscreen(false); shellSurface->setMaximized(false); - QVERIFY(fullscreenChangedSpy.wait()); - if (decoMode == ServerSideDecoration::Mode::Server) { - QVERIFY(sizeChangeRequestedSpy.wait()); - // XDG will legitimately get two updates. They might be batched - if (shellSurface && sizeChangeRequestedSpy.count() == 1) { - QVERIFY(sizeChangeRequestedSpy.wait()); - } - 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); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 4); + QCOMPARE(configureRequestedSpy.last().at(0).value(), QSize(100, 50)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!(states & XdgShellSurface::State::Maximized)); + QVERIFY(!(states & XdgShellSurface::State::Fullscreen)); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::red); + + QVERIFY(frameGeometryChangedSpy.wait()); + QVERIFY(!client->isFullScreen()); + QCOMPARE(client->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QCOMPARE(client->maximizeMode(), MaximizeRestore); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClient::testWindowOpensLargerThanScreen_data() { QTest::addColumn("type"); QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testWindowOpensLargerThanScreen() { // this test creates a window which is as large as the screen, but is decorated // the window should get resized to fit into the screen, BUG: 366632 QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(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(10)); + QCOMPARE(c->frameGeometry(), QRect(QPoint(0, 0), screens()->size(0))); } void TestXdgShellClient::testHidden_data() { QTest::addColumn("type"); QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testHidden() { // this test verifies that when hiding window it doesn't get shown QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(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 TestXdgShellClient::testDesktopFileName() { QIcon::setThemeName(QStringLiteral("breeze")); // this test verifies that desktop file name is passed correctly to the window QScopedPointer surface(Test::createSurface()); // only xdg-shell as ShellSurface misses the setter QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); shellSurface->setAppId(QByteArrayLiteral("org.kde.foo")); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->desktopFileName(), QByteArrayLiteral("org.kde.foo")); QCOMPARE(c->resourceClass(), QByteArrayLiteral("org.kde.foo")); QVERIFY(c->resourceName().startsWith("testXdgShellClient")); // the desktop file does not exist, so icon should be generic Wayland QCOMPARE(c->icon().name(), QStringLiteral("wayland")); QSignalSpy desktopFileNameChangedSpy(c, &AbstractClient::desktopFileNameChanged); QVERIFY(desktopFileNameChangedSpy.isValid()); QSignalSpy iconChangedSpy(c, &AbstractClient::iconChanged); QVERIFY(iconChangedSpy.isValid()); shellSurface->setAppId(QByteArrayLiteral("org.kde.bar")); QVERIFY(desktopFileNameChangedSpy.wait()); QCOMPARE(c->desktopFileName(), QByteArrayLiteral("org.kde.bar")); QCOMPARE(c->resourceClass(), QByteArrayLiteral("org.kde.bar")); QVERIFY(c->resourceName().startsWith("testXdgShellClient")); // icon should still be wayland QCOMPARE(c->icon().name(), QStringLiteral("wayland")); QVERIFY(iconChangedSpy.isEmpty()); const QString dfPath = QFINDTESTDATA("data/example.desktop"); shellSurface->setAppId(dfPath.toUtf8()); QVERIFY(desktopFileNameChangedSpy.wait()); QCOMPARE(iconChangedSpy.count(), 1); QCOMPARE(QString::fromUtf8(c->desktopFileName()), dfPath); QCOMPARE(c->icon().name(), QStringLiteral("kwin")); } void TestXdgShellClient::testCaptionSimplified() { // this test verifies that caption is properly trimmed // see BUG 323798 comment #12 QScopedPointer surface(Test::createSurface()); // only done for xdg-shell as ShellSurface misses the setter QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); const QString origTitle = QString::fromUtf8(QByteArrayLiteral("Was tun, wenn Schüler Autismus haben?\342\200\250\342\200\250\342\200\250 – Marlies Hübner - Mozilla Firefox")); shellSurface->setTitle(origTitle); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(c->caption() != origTitle); QCOMPARE(c->caption(), origTitle.simplified()); } void TestXdgShellClient::testCaptionMultipleWindows() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); shellSurface->setTitle(QStringLiteral("foo")); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QCOMPARE(c->caption(), QStringLiteral("foo")); QCOMPARE(c->captionNormal(), QStringLiteral("foo")); QCOMPARE(c->captionSuffix(), QString()); QScopedPointer surface2(Test::createSurface()); QScopedPointer shellSurface2(Test::createXdgShellStableSurface(surface2.data())); shellSurface2->setTitle(QStringLiteral("foo")); auto c2 = Test::renderAndWaitForShown(surface2.data(), QSize(100, 50), Qt::blue); QVERIFY(c2); QCOMPARE(c2->caption(), QStringLiteral("foo <2>")); QCOMPARE(c2->captionNormal(), QStringLiteral("foo")); QCOMPARE(c2->captionSuffix(), QStringLiteral(" <2>")); QScopedPointer surface3(Test::createSurface()); QScopedPointer shellSurface3(Test::createXdgShellStableSurface(surface3.data())); shellSurface3->setTitle(QStringLiteral("foo")); auto c3 = Test::renderAndWaitForShown(surface3.data(), QSize(100, 50), Qt::blue); QVERIFY(c3); QCOMPARE(c3->caption(), QStringLiteral("foo <3>")); QCOMPARE(c3->captionNormal(), QStringLiteral("foo")); QCOMPARE(c3->captionSuffix(), QStringLiteral(" <3>")); QScopedPointer surface4(Test::createSurface()); QScopedPointer shellSurface4(Test::createXdgShellStableSurface(surface4.data())); shellSurface4->setTitle(QStringLiteral("bar")); auto c4 = Test::renderAndWaitForShown(surface4.data(), QSize(100, 50), Qt::blue); QVERIFY(c4); QCOMPARE(c4->caption(), QStringLiteral("bar")); QCOMPARE(c4->captionNormal(), QStringLiteral("bar")); QCOMPARE(c4->captionSuffix(), QString()); QSignalSpy captionChangedSpy(c4, &AbstractClient::captionChanged); QVERIFY(captionChangedSpy.isValid()); shellSurface4->setTitle(QStringLiteral("foo")); QVERIFY(captionChangedSpy.wait()); QCOMPARE(captionChangedSpy.count(), 1); QCOMPARE(c4->caption(), QStringLiteral("foo <4>")); QCOMPARE(c4->captionNormal(), QStringLiteral("foo")); QCOMPARE(c4->captionSuffix(), QStringLiteral(" <4>")); } void TestXdgShellClient::testUnresponsiveWindow_data() { QTest::addColumn("shellInterface");//see env selection in qwaylandintegration.cpp QTest::addColumn("socketMode"); QTest::newRow("xdg display") << "xdg-shell" << false; QTest::newRow("xdg socket") << "xdg-shell" << true; } void TestXdgShellClient::testUnresponsiveWindow() { // this test verifies that killWindow properly terminates a process // for this an external binary is launched const QString kill = QFINDTESTDATA(QStringLiteral("kill")); QVERIFY(!kill.isEmpty()); QSignalSpy 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); QSignalSpy processStartedSpy{process.data(), &QProcess::started}; QVERIFY(processStartedSpy.isValid()); process->start(); QVERIFY(processStartedSpy.wait()); AbstractClient *killClient = nullptr; if (shellClientAddedSpy.isEmpty()) { QVERIFY(shellClientAddedSpy.wait()); } ::kill(process->processId(), SIGUSR1); // send a signal to freeze the process killClient = shellClientAddedSpy.first().first().value(); QVERIFY(killClient); QSignalSpy unresponsiveSpy(killClient, &AbstractClient::unresponsiveChanged); QSignalSpy killedSpy(process.data(), static_cast(&QProcess::finished)); QSignalSpy deletedSpy(killClient, &QObject::destroyed); qint64 startTime = QDateTime::currentMSecsSinceEpoch(); //wait for the process to be frozen QTest::qWait(10); //pretend the user clicked the close button killClient->closeWindow(); //client should not yet be marked unresponsive nor killed QVERIFY(!killClient->unresponsive()); QVERIFY(killedSpy.isEmpty()); QVERIFY(unresponsiveSpy.wait()); //client should be marked unresponsive but not killed auto elapsed1 = QDateTime::currentMSecsSinceEpoch() - startTime; QVERIFY(elapsed1 > 900 && elapsed1 < 1200); //ping timer is 1s, but coarse timers on a test across two processes means we need a fuzzy compare QVERIFY(killClient->unresponsive()); QVERIFY(killedSpy.isEmpty()); QVERIFY(deletedSpy.wait()); if (!socketMode) { //process was killed - because we're across process this could happen in either order QVERIFY(killedSpy.count() || killedSpy.wait()); } auto elapsed2 = QDateTime::currentMSecsSinceEpoch() - startTime; QVERIFY(elapsed2 > 1800); //second ping comes in a second later } void TestXdgShellClient::testX11WindowId_data() { QTest::addColumn("type"); QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testX11WindowId() { QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(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 TestXdgShellClient::testAppMenu() { //register a faux appmenu client QVERIFY (QDBusConnection::sessionBus().registerService("org.kde.kappmenu")); QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QScopedPointer menu(Test::waylandAppMenuManager()->create(surface.data())); QSignalSpy spy(c, &AbstractClient::hasApplicationMenuChanged); menu->setAddress("service.name", "object/path"); spy.wait(); QCOMPARE(c->hasApplicationMenu(), true); QCOMPARE(c->applicationMenuServiceName(), QString("service.name")); QCOMPARE(c->applicationMenuObjectPath(), QString("object/path")); QVERIFY (QDBusConnection::sessionBus().unregisterService("org.kde.kappmenu")); } void TestXdgShellClient::testNoDecorationModeRequested_data() { QTest::addColumn("type"); QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testNoDecorationModeRequested() { // this test verifies that the decoration follows the default mode if no mode is explicitly requested QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(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); } void TestXdgShellClient::testSendClientWithTransientToDesktop_data() { QTest::addColumn("type"); QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testSendClientWithTransientToDesktop() { // this test verifies that when sending a client to a desktop all transients are also send to that desktop VirtualDesktopManager::self()->setCount(2); QScopedPointer surface{Test::createSurface()}; QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); // let's create a transient window QScopedPointer transientSurface{Test::createSurface()}; QScopedPointer transientShellSurface(Test::createXdgShellSurface(type, transientSurface.data())); transientShellSurface->setTransientFor(shellSurface.data()); auto transient = Test::renderAndWaitForShown(transientSurface.data(), QSize(100, 50), Qt::blue); QVERIFY(transient); QCOMPARE(workspace()->activeClient(), transient); QCOMPARE(transient->transientFor(), c); QVERIFY(c->transients().contains(transient)); QCOMPARE(c->desktop(), 1); QVERIFY(!c->isOnAllDesktops()); QCOMPARE(transient->desktop(), 1); QVERIFY(!transient->isOnAllDesktops()); workspace()->slotWindowToDesktop(2); QCOMPARE(c->desktop(), 1); QCOMPARE(transient->desktop(), 2); // activate c workspace()->activateClient(c); QCOMPARE(workspace()->activeClient(), c); QVERIFY(c->isActive()); // and send it to the desktop it's already on QCOMPARE(c->desktop(), 1); QCOMPARE(transient->desktop(), 2); workspace()->slotWindowToDesktop(1); // which should move the transient back to the desktop QCOMPARE(c->desktop(), 1); QCOMPARE(transient->desktop(), 1); } void TestXdgShellClient::testMinimizeWindowWithTransients_data() { QTest::addColumn("type"); QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; } void TestXdgShellClient::testMinimizeWindowWithTransients() { // this test verifies that when minimizing/unminimizing a window all its // transients will be minimized/unminimized as well // create the main window QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(c); QVERIFY(!c->isMinimized()); // create a transient window QScopedPointer transientSurface(Test::createSurface()); QScopedPointer transientShellSurface(Test::createXdgShellSurface(type, transientSurface.data())); transientShellSurface->setTransientFor(shellSurface.data()); auto transient = Test::renderAndWaitForShown(transientSurface.data(), QSize(100, 50), Qt::red); QVERIFY(transient); QVERIFY(!transient->isMinimized()); QCOMPARE(transient->transientFor(), c); QVERIFY(c->hasTransient(transient, false)); // minimize the main window, the transient should be minimized as well c->minimize(); QVERIFY(c->isMinimized()); QVERIFY(transient->isMinimized()); // unminimize the main window, the transient should be unminimized as well c->unminimize(); QVERIFY(!c->isMinimized()); QVERIFY(!transient->isMinimized()); } void TestXdgShellClient::testXdgDecoration_data() { QTest::addColumn("requestedMode"); QTest::addColumn("expectedMode"); QTest::newRow("client side requested") << XdgDecoration::Mode::ClientSide << XdgDecoration::Mode::ClientSide; QTest::newRow("server side requested") << XdgDecoration::Mode::ServerSide << XdgDecoration::Mode::ServerSide; } void TestXdgShellClient::testXdgDecoration() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QScopedPointer deco(Test::xdgDecorationManager()->getToplevelDecoration(shellSurface.data())); QSignalSpy decorationConfiguredSpy(deco.data(), &XdgDecoration::modeChanged); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QFETCH(KWayland::Client::XdgDecoration::Mode, requestedMode); QFETCH(KWayland::Client::XdgDecoration::Mode, expectedMode); //request a mode deco->setMode(requestedMode); //kwin will send a configure decorationConfiguredSpy.wait(); configureRequestedSpy.wait(); QCOMPARE(decorationConfiguredSpy.count(), 1); QCOMPARE(decorationConfiguredSpy.first()[0].value(), expectedMode); QVERIFY(configureRequestedSpy.count() > 0); shellSurface->ackConfigure(configureRequestedSpy.last()[2].toInt()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); QCOMPARE(c->userCanSetNoBorder(), expectedMode == XdgDecoration::Mode::ServerSide); QCOMPARE(c->isDecorated(), expectedMode == XdgDecoration::Mode::ServerSide); } void TestXdgShellClient::testXdgNeverCommitted() { //check we don't crash if we create a shell object but delete the XdgShellClient before committing it QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); } void TestXdgShellClient::testXdgInitialState() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); QCOMPARE(configureRequestedSpy.count(), 1); const auto size = configureRequestedSpy.first()[0].value(); QCOMPARE(size, QSize(0, 0)); //client should chose it's preferred size shellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); auto c = Test::renderAndWaitForShown(surface.data(), QSize(200,100), Qt::blue); QCOMPARE(c->size(), QSize(200, 100)); } void TestXdgShellClient::testXdgInitiallyMaximised() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); shellSurface->setMaximized(true); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); QCOMPARE(configureRequestedSpy.count(), 1); const auto size = configureRequestedSpy.first()[0].value(); const auto state = configureRequestedSpy.first()[1].value(); QCOMPARE(size, QSize(1280, 1024)); QVERIFY(state & KWayland::Client::XdgShellSurface::State::Maximized); shellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); QCOMPARE(c->maximizeMode(), MaximizeFull); QCOMPARE(c->size(), QSize(1280, 1024)); } void TestXdgShellClient::testXdgInitiallyFullscreen() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); shellSurface->setFullscreen(true); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); QCOMPARE(configureRequestedSpy.count(), 1); const auto size = configureRequestedSpy.first()[0].value(); const auto state = configureRequestedSpy.first()[1].value(); QCOMPARE(size, QSize(1280, 1024)); QVERIFY(state & KWayland::Client::XdgShellSurface::State::Fullscreen); shellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue); QCOMPARE(c->isFullScreen(), true); QCOMPARE(c->size(), QSize(1280, 1024)); } void TestXdgShellClient::testXdgInitiallyMinimized() { QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); shellSurface->requestMinimize(); surface->commit(Surface::CommitFlag::None); configureRequestedSpy.wait(); QCOMPARE(configureRequestedSpy.count(), 1); const auto size = configureRequestedSpy.first()[0].value(); const auto state = configureRequestedSpy.first()[1].value(); QCOMPARE(size, QSize(0, 0)); QCOMPARE(state, 0); shellSurface->ackConfigure(configureRequestedSpy.first()[2].toUInt()); QEXPECT_FAIL("", "Client created in a minimised state is not exposed to kwin bug 404838", Abort); auto c = Test::renderAndWaitForShown(surface.data(), size, Qt::blue, QImage::Format_ARGB32, 10); QVERIFY(c); QVERIFY(c->isMinimized()); } void TestXdgShellClient::testXdgWindowGeometryIsntSet() { // This test verifies that the effective window geometry corresponds to the // bounding rectangle of the main surface and its sub-surfaces if no window // geometry is set by the client. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); const QPoint oldPosition = client->pos(); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); Test::render(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(100, 50)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); QScopedPointer childSurface(Test::createSurface()); QScopedPointer subSurface(Test::createSubSurface(childSurface.data(), surface.data())); QVERIFY(subSurface); subSurface->setPosition(QPoint(-20, -10)); Test::render(childSurface.data(), QSize(100, 50), Qt::blue); surface->commit(Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(120, 60)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition + QPoint(20, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); } void TestXdgShellClient::testXdgWindowGeometryAttachBuffer() { // This test verifies that the effective window geometry remains the same when // a new buffer is attached and xdg_surface.set_window_geometry is not called // again. Notice that the window geometry must remain the same even if the new // buffer is smaller. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); const QPoint oldPosition = client->pos(); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); surface->commit(Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 1); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); Test::render(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 2); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); - QCOMPARE(client->frameGeometry().size(), QSize(100, 50)); + QCOMPARE(client->frameGeometry().size(), QSize(90, 40)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); - shellSurface->setWindowGeometry(QRect(5, 5, 90, 40)); + shellSurface->setWindowGeometry(QRect(0, 0, 100, 50)); surface->commit(Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 3); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); - QCOMPARE(client->frameGeometry().size(), QSize(90, 40)); - QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(5, 5)); + QCOMPARE(client->frameGeometry().size(), QSize(100, 50)); + QCOMPARE(client->bufferGeometry().topLeft(), oldPosition); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClient::testXdgWindowGeometryAttachSubSurface() { // This test verifies that the effective window geometry remains the same // when a new sub-surface is added and xdg_surface.set_window_geometry is // not called again. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); const QPoint oldPosition = client->pos(); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); surface->commit(Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QScopedPointer childSurface(Test::createSurface()); QScopedPointer subSurface(Test::createSubSurface(childSurface.data(), surface.data())); QVERIFY(subSurface); subSurface->setPosition(QPoint(-20, -20)); Test::render(childSurface.data(), QSize(100, 50), Qt::blue); surface->commit(Surface::CommitFlag::None); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); shellSurface->setWindowGeometry(QRect(-15, -15, 50, 40)); surface->commit(Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); QCOMPARE(client->frameGeometry().size(), QSize(50, 40)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(-15, -15)); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); } void TestXdgShellClient::testXdgWindowGeometryInteractiveResize() { // This test verifies that correct window geometry is provided along each // configure event when an xdg-shell is being interactively resized. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); surface->commit(Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); QVERIFY(clientStartMoveResizedSpy.isValid()); QSignalSpy clientStepUserMovedResizedSpy(client, &AbstractClient::clientStepUserMovedResized); QVERIFY(clientStepUserMovedResizedSpy.isValid()); QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); QVERIFY(clientFinishUserMovedResizedSpy.isValid()); // Start interactively resizing the client. QCOMPARE(workspace()->moveResizeClient(), nullptr); workspace()->slotWindowResize(); QCOMPARE(workspace()->moveResizeClient(), client); QCOMPARE(clientStartMoveResizedSpy.count(), 1); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); XdgShellSurface::States states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); // Go right. QPoint cursorPos = KWin::Cursors::self()->mouse()->pos(); client->keyPressEvent(Qt::Key_Right); client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(8, 0)); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(188, 80)); shellSurface->setWindowGeometry(QRect(10, 10, 188, 80)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(208, 100), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(clientStepUserMovedResizedSpy.count(), 1); QCOMPARE(client->bufferGeometry().size(), QSize(208, 100)); QCOMPARE(client->frameGeometry().size(), QSize(188, 80)); // Go down. cursorPos = KWin::Cursors::self()->mouse()->pos(); client->keyPressEvent(Qt::Key_Down); client->updateMoveResize(KWin::Cursors::self()->mouse()->pos()); QCOMPARE(KWin::Cursors::self()->mouse()->pos(), cursorPos + QPoint(0, 8)); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 4); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Resizing)); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(188, 88)); shellSurface->setWindowGeometry(QRect(10, 10, 188, 88)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(208, 108), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(clientStepUserMovedResizedSpy.count(), 2); QCOMPARE(client->bufferGeometry().size(), QSize(208, 108)); QCOMPARE(client->frameGeometry().size(), QSize(188, 88)); // Finish resizing the client. client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); -#if 0 - QEXPECT_FAIL("", "XdgShellClient currently doesn't send final configure event", Abort); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 5); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); -#endif shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClient::testXdgWindowGeometryFullScreen() { // This test verifies that an xdg-shell receives correct window geometry when // its fullscreen state gets changed. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); surface->commit(Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); workspace()->slotWindowFullScreen(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); XdgShellSurface::States states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Fullscreen)); shellSurface->setWindowGeometry(QRect(0, 0, 1280, 1024)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(1280, 1024)); QCOMPARE(client->frameGeometry().size(), QSize(1280, 1024)); workspace()->slotWindowFullScreen(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(180, 80)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Fullscreen)); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(200, 100), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClient::testXdgWindowGeometryMaximize() { // This test verifies that an xdg-shell receives correct window geometry when // its maximized state gets changed. QScopedPointer surface(Test::createSurface()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(200, 100), Qt::red); QVERIFY(client); QVERIFY(client->isActive()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(200, 100)); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 1); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); surface->commit(Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); XdgShellSurface::States states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->setWindowGeometry(QRect(0, 0, 1280, 1024)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(1280, 1024), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(1280, 1024)); QCOMPARE(client->frameGeometry().size(), QSize(1280, 1024)); workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 3); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(180, 80)); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Maximized)); shellSurface->setWindowGeometry(QRect(10, 10, 180, 80)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); Test::render(surface.data(), QSize(200, 100), Qt::blue); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(client->bufferGeometry().size(), QSize(200, 100)); QCOMPARE(client->frameGeometry().size(), QSize(180, 80)); shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); } WAYLANDTEST_MAIN(TestXdgShellClient) #include "xdgshellclient_test.moc" diff --git a/deleted.cpp b/deleted.cpp index cee6bebaa..ca0cef866 100644 --- a/deleted.cpp +++ b/deleted.cpp @@ -1,317 +1,317 @@ /******************************************************************** 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 "x11client.h" #include "group.h" #include "netinfo.h" #include "shadow.h" -#include "xdgshellclient.h" +#include "waylandclient.h" #include "decorations/decoratedclient.h" #include "decorations/decorationrenderer.h" #include namespace KWin { Deleted::Deleted() : Toplevel() , delete_refcount(1) , m_frame(XCB_WINDOW_NONE) , no_border(true) , m_layer(UnknownLayer) , m_minimized(false) , m_modal(false) , m_wasClient(false) , m_decorationRenderer(nullptr) , m_fullscreen(false) , m_keepAbove(false) , m_keepBelow(false) , m_wasActive(false) , m_wasX11Client(false) , m_wasWaylandClient(false) , m_wasGroupTransient(false) , m_wasPopupWindow(false) , m_wasOutline(false) { } Deleted::~Deleted() { if (delete_refcount != 0) qCCritical(KWIN_CORE) << "Deleted client has non-zero reference count (" << delete_refcount << ")"; Q_ASSERT(delete_refcount == 0); if (workspace()) { workspace()->removeDeleted(this); } for (Toplevel *toplevel : qAsConst(m_transientFor)) { if (auto *deleted = qobject_cast(toplevel)) { deleted->removeTransient(this); } } for (Deleted *transient : qAsConst(m_transients)) { transient->removeTransientFor(this); } deleteEffectWindow(); } Deleted* Deleted::create(Toplevel* c) { Deleted* d = new Deleted(); d->copyToDeleted(c); workspace()->addDeleted(d, c); return d; } // to be used only from Workspace::finishCompositing() void Deleted::discard() { delete_refcount = 0; delete this; } void Deleted::copyToDeleted(Toplevel* c) { Q_ASSERT(dynamic_cast< Deleted* >(c) == nullptr); Toplevel::copyToDeleted(c); m_bufferGeometry = c->bufferGeometry(); m_bufferMargins = c->bufferMargins(); m_frameMargins = c->frameMargins(); m_bufferScale = c->bufferScale(); desk = c->desktop(); m_desktops = c->desktops(); activityList = c->activities(); contentsRect = QRect(c->clientPos(), c->clientSize()); m_contentPos = c->clientContentPos(); transparent_rect = c->transparentRect(); m_layer = c->layer(); m_frame = c->frameId(); m_opacity = c->opacity(); m_type = c->windowType(); m_windowRole = c->windowRole(); if (WinInfo* cinfo = dynamic_cast< WinInfo* >(info)) cinfo->disable(); if (AbstractClient *client = dynamic_cast(c)) { no_border = client->noBorder(); if (!no_border) { client->layoutDecorationRects(decoration_left, decoration_top, decoration_right, decoration_bottom); if (client->isDecorated()) { if (Decoration::Renderer *renderer = client->decoratedClient()->renderer()) { m_decorationRenderer = renderer; m_decorationRenderer->reparent(this); } } } m_wasClient = true; m_minimized = client->isMinimized(); m_modal = client->isModal(); m_mainClients = client->mainClients(); foreach (AbstractClient *c, m_mainClients) { addTransientFor(c); connect(c, &AbstractClient::windowClosed, this, &Deleted::mainClientClosed); } m_fullscreen = client->isFullScreen(); m_keepAbove = client->keepAbove(); m_keepBelow = client->keepBelow(); m_caption = client->caption(); m_wasActive = client->isActive(); m_wasGroupTransient = client->groupTransient(); } for (auto vd : m_desktops) { connect(vd, &QObject::destroyed, this, [=] { m_desktops.removeOne(vd); }); } - m_wasWaylandClient = qobject_cast(c) != nullptr; + m_wasWaylandClient = qobject_cast(c) != nullptr; m_wasX11Client = qobject_cast(c) != nullptr; m_wasPopupWindow = c->isPopupWindow(); m_wasOutline = c->isOutline(); } void Deleted::unrefWindow() { if (--delete_refcount > 0) return; // needs to be delayed // a) when calling from effects, otherwise it'd be rather complicated to handle the case of the // window going away during a painting pass // b) to prevent dangeling pointers in the stacking order, see bug #317765 deleteLater(); } QRect Deleted::bufferGeometry() const { return m_bufferGeometry; } QMargins Deleted::bufferMargins() const { return m_bufferMargins; } QMargins Deleted::frameMargins() const { return m_frameMargins; } qreal Deleted::bufferScale() const { return m_bufferScale; } int Deleted::desktop() const { return desk; } QStringList Deleted::activities() const { return activityList; } QVector Deleted::desktops() const { return m_desktops; } QPoint Deleted::clientPos() const { return contentsRect.topLeft(); } 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::transparentRect() const { return transparent_rect; } bool Deleted::isDeleted() const { return true; } NET::WindowType Deleted::windowType(bool direct, int supportedTypes) const { Q_UNUSED(direct) Q_UNUSED(supportedTypes) return m_type; } void Deleted::mainClientClosed(Toplevel *client) { if (AbstractClient *c = dynamic_cast(client)) m_mainClients.removeAll(c); } void Deleted::transientForClosed(Toplevel *toplevel, Deleted *deleted) { if (deleted == nullptr) { m_transientFor.removeAll(toplevel); return; } const int index = m_transientFor.indexOf(toplevel); if (index == -1) { return; } m_transientFor[index] = deleted; deleted->addTransient(this); } xcb_window_t Deleted::frameId() const { return m_frame; } double Deleted::opacity() const { return m_opacity; } QByteArray Deleted::windowRole() const { return m_windowRole; } QVector Deleted::x11DesktopIds() const { const auto desks = desktops(); QVector x11Ids; x11Ids.reserve(desks.count()); std::transform(desks.constBegin(), desks.constEnd(), std::back_inserter(x11Ids), [] (const VirtualDesktop *vd) { return vd->x11DesktopNumber(); } ); return x11Ids; } void Deleted::addTransient(Deleted *transient) { m_transients.append(transient); } void Deleted::removeTransient(Deleted *transient) { m_transients.removeAll(transient); } void Deleted::addTransientFor(AbstractClient *parent) { m_transientFor.append(parent); connect(parent, &AbstractClient::windowClosed, this, &Deleted::transientForClosed); } void Deleted::removeTransientFor(Deleted *parent) { m_transientFor.removeAll(parent); } } // namespace diff --git a/effects.cpp b/effects.cpp index eee26e987..e44c21546 100644 --- a/effects.cpp +++ b/effects.cpp @@ -1,2416 +1,2416 @@ /******************************************************************** 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 "x11client.h" #include "cursor.h" #include "group.h" #include "internal_client.h" #include "osd.h" #include "pointer_input.h" #include "unmanaged.h" #ifdef KWIN_BUILD_TABBOX #include "tabbox.h" #endif #include "screenedge.h" #include "scripting/scriptedeffect.h" #include "screens.h" #include "screenlockerwatcher.h" #include "thumbnailitem.h" #include "virtualdesktops.h" #include "window_property_notify_x11_filter.h" #include "workspace.h" #include "kwinglutils.h" #include "kwineffectquickview.h" #include #include #include "composite.h" #include "xcbutils.h" #include "platform.h" -#include "xdgshellclient.h" +#include "waylandclient.h" #include "wayland_server.h" #include "decorations/decorationbridge.h" #include namespace KWin { //--------------------- // Static static QByteArray readWindowProperty(xcb_window_t win, xcb_atom_t atom, xcb_atom_t type, int format) { if (win == XCB_WINDOW_NONE) { return QByteArray(); } uint32_t len = 32768; for (;;) { Xcb::Property prop(false, win, atom, XCB_ATOM_ANY, 0, len); if (prop.isNull()) { // get property failed return QByteArray(); } if (prop->bytes_after > 0) { len *= 2; continue; } return prop.toByteArray(format, type); } } static void deleteWindowProperty(Window win, long int atom) { if (win == XCB_WINDOW_NONE) { return; } xcb_delete_property(kwinApp()->x11Connection(), win, atom); } static xcb_atom_t registerSupportProperty(const QByteArray &propertyName) { auto c = kwinApp()->x11Connection(); if (!c) { return XCB_ATOM_NONE; } // get the atom for the propertyName ScopedCPointer atomReply(xcb_intern_atom_reply(c, xcb_intern_atom_unchecked(c, false, propertyName.size(), propertyName.constData()), nullptr)); if (atomReply.isNull()) { return XCB_ATOM_NONE; } // announce property on root window unsigned char dummy = 0; xcb_change_property(c, XCB_PROP_MODE_REPLACE, kwinApp()->x11RootWindow(), atomReply->atom, atomReply->atom, 8, 1, &dummy); // TODO: add to _NET_SUPPORTED return atomReply->atom; } //--------------------- EffectsHandlerImpl::EffectsHandlerImpl(Compositor *compositor, Scene *scene) : EffectsHandler(scene->compositingType()) , keyboard_grab_effect(nullptr) , fullscreen_effect(nullptr) , next_window_quad_type(EFFECT_QUAD_TYPE_START) , m_compositor(compositor) , m_scene(scene) , m_desktopRendering(false) , m_currentRenderedDesktop(0) , m_effectLoader(new EffectLoader(this)) , m_trackingCursorChanges(0) { qRegisterMetaType>(); connect(m_effectLoader, &AbstractEffectLoader::effectLoaded, this, [this](Effect *effect, const QString &name) { effect_order.insert(effect->requestedEffectChainPosition(), EffectPair(name, effect)); loaded_effects << EffectPair(name, effect); effectsChanged(); } ); m_effectLoader->setConfig(kwinApp()->config()); new EffectsAdaptor(this); QDBusConnection dbus = QDBusConnection::sessionBus(); dbus.registerObject(QStringLiteral("/Effects"), this); // init is important, otherwise causes crashes when quads are build before the first painting pass start m_currentBuildQuadsIterator = m_activeEffects.constEnd(); Workspace *ws = Workspace::self(); VirtualDesktopManager *vds = VirtualDesktopManager::self(); connect(ws, &Workspace::showingDesktopChanged, this, &EffectsHandlerImpl::showingDesktopChanged); connect(ws, &Workspace::currentDesktopChanged, this, [this](int old, AbstractClient *c) { const int newDesktop = VirtualDesktopManager::self()->current(); if (old != 0 && newDesktop != old) { emit desktopChanged(old, newDesktop, c ? c->effectWindow() : nullptr); // TODO: remove in 4.10 emit desktopChanged(old, newDesktop); } } ); connect(ws, &Workspace::desktopPresenceChanged, this, [this](AbstractClient *c, int old) { if (!c->effectWindow()) { return; } emit desktopPresenceChanged(c->effectWindow(), old, c->desktop()); } ); connect(ws, &Workspace::clientAdded, this, [this](AbstractClient *c) { if (c->readyForPainting()) slotClientShown(c); else connect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotClientShown); } ); connect(ws, &Workspace::unmanagedAdded, this, [this](Unmanaged *u) { // it's never initially ready but has synthetic 50ms delay connect(u, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotUnmanagedShown); } ); connect(ws, &Workspace::internalClientAdded, this, [this](InternalClient *client) { setupClientConnections(client); emit windowAdded(client->effectWindow()); } ); connect(ws, &Workspace::clientActivated, this, [this](KWin::AbstractClient *c) { emit windowActivated(c ? c->effectWindow() : nullptr); } ); connect(ws, &Workspace::deletedRemoved, this, [this](KWin::Deleted *d) { emit windowDeleted(d->effectWindow()); elevated_windows.removeAll(d->effectWindow()); } ); connect(ws->sessionManager(), &SessionManager::stateChanged, this, &KWin::EffectsHandler::sessionStateChanged); connect(vds, &VirtualDesktopManager::countChanged, this, &EffectsHandler::numberDesktopsChanged); connect(Cursors::self()->mouse(), &Cursor::mouseChanged, this, &EffectsHandler::mouseChanged); connect(screens(), &Screens::countChanged, this, &EffectsHandler::numberScreensChanged); connect(screens(), &Screens::sizeChanged, this, &EffectsHandler::virtualScreenSizeChanged); connect(screens(), &Screens::geometryChanged, this, &EffectsHandler::virtualScreenGeometryChanged); #ifdef KWIN_BUILD_ACTIVITIES if (Activities *activities = Activities::self()) { connect(activities, &Activities::added, this, &EffectsHandler::activityAdded); connect(activities, &Activities::removed, this, &EffectsHandler::activityRemoved); connect(activities, &Activities::currentChanged, this, &EffectsHandler::currentActivityChanged); } #endif connect(ws, &Workspace::stackingOrderChanged, this, &EffectsHandler::stackingOrderChanged); #ifdef KWIN_BUILD_TABBOX TabBox::TabBox *tabBox = TabBox::TabBox::self(); connect(tabBox, &TabBox::TabBox::tabBoxAdded, this, &EffectsHandler::tabBoxAdded); connect(tabBox, &TabBox::TabBox::tabBoxUpdated, this, &EffectsHandler::tabBoxUpdated); connect(tabBox, &TabBox::TabBox::tabBoxClosed, this, &EffectsHandler::tabBoxClosed); connect(tabBox, &TabBox::TabBox::tabBoxKeyEvent, this, &EffectsHandler::tabBoxKeyEvent); #endif connect(ScreenEdges::self(), &ScreenEdges::approaching, this, &EffectsHandler::screenEdgeApproaching); connect(ScreenLockerWatcher::self(), &ScreenLockerWatcher::locked, this, &EffectsHandler::screenLockingChanged); connect(ScreenLockerWatcher::self(), &ScreenLockerWatcher::aboutToLock, this, &EffectsHandler::screenAboutToLock); connect(kwinApp(), &Application::x11ConnectionChanged, this, [this] { registered_atoms.clear(); for (auto it = m_propertiesForEffects.keyBegin(); it != m_propertiesForEffects.keyEnd(); it++) { const auto atom = registerSupportProperty(*it); if (atom == XCB_ATOM_NONE) { continue; } m_compositor->keepSupportProperty(atom); m_managedProperties.insert(*it, atom); registerPropertyType(atom, true); } if (kwinApp()->x11Connection()) { m_x11WindowPropertyNotify = std::make_unique(this); } else { m_x11WindowPropertyNotify.reset(); } emit xcbConnectionChanged(); } ); if (kwinApp()->x11Connection()) { m_x11WindowPropertyNotify = std::make_unique(this); } // connect all clients for (X11Client *c : ws->clientList()) { setupClientConnections(c); } for (Unmanaged *u : ws->unmanagedList()) { setupUnmanagedConnections(u); } for (InternalClient *client : ws->internalClients()) { setupClientConnections(client); } if (auto w = waylandServer()) { connect(w, &WaylandServer::shellClientAdded, this, [this](AbstractClient *c) { if (c->readyForPainting()) slotWaylandClientShown(c); else connect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotWaylandClientShown); }); const auto clients = waylandServer()->clients(); for (AbstractClient *c : clients) { if (c->readyForPainting()) { setupClientConnections(c); } else { connect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotWaylandClientShown); } } } reconfigure(); } EffectsHandlerImpl::~EffectsHandlerImpl() { unloadAllEffects(); } void EffectsHandlerImpl::unloadAllEffects() { for (const EffectPair &pair : loaded_effects) { destroyEffect(pair.second); } effect_order.clear(); m_effectLoader->clear(); effectsChanged(); } void EffectsHandlerImpl::setupClientConnections(AbstractClient* c) { connect(c, &AbstractClient::windowClosed, this, &EffectsHandlerImpl::slotWindowClosed); connect(c, static_cast(&AbstractClient::clientMaximizedStateChanged), this, &EffectsHandlerImpl::slotClientMaximized); connect(c, &AbstractClient::clientStartUserMovedResized, this, [this](AbstractClient *c) { emit windowStartUserMovedResized(c->effectWindow()); } ); connect(c, &AbstractClient::clientStepUserMovedResized, this, [this](AbstractClient *c, const QRect &geometry) { emit windowStepUserMovedResized(c->effectWindow(), geometry); } ); connect(c, &AbstractClient::clientFinishUserMovedResized, this, [this](AbstractClient *c) { emit windowFinishUserMovedResized(c->effectWindow()); } ); connect(c, &AbstractClient::opacityChanged, this, &EffectsHandlerImpl::slotOpacityChanged); connect(c, &AbstractClient::clientMinimized, this, [this](AbstractClient *c, bool animate) { // TODO: notify effects even if it should not animate? if (animate) { emit windowMinimized(c->effectWindow()); } } ); connect(c, &AbstractClient::clientUnminimized, this, [this](AbstractClient* c, bool animate) { // TODO: notify effects even if it should not animate? if (animate) { emit windowUnminimized(c->effectWindow()); } } ); connect(c, &AbstractClient::modalChanged, this, &EffectsHandlerImpl::slotClientModalityChanged); connect(c, &AbstractClient::geometryShapeChanged, this, &EffectsHandlerImpl::slotGeometryShapeChanged); connect(c, &AbstractClient::frameGeometryChanged, this, &EffectsHandlerImpl::slotFrameGeometryChanged); connect(c, &AbstractClient::damaged, this, &EffectsHandlerImpl::slotWindowDamaged); connect(c, &AbstractClient::paddingChanged, this, &EffectsHandlerImpl::slotPaddingChanged); connect(c, &AbstractClient::unresponsiveChanged, this, [this, c](bool unresponsive) { emit windowUnresponsiveChanged(c->effectWindow(), unresponsive); } ); connect(c, &AbstractClient::windowShown, this, [this](Toplevel *c) { emit windowShown(c->effectWindow()); } ); connect(c, &AbstractClient::windowHidden, this, [this](Toplevel *c) { emit windowHidden(c->effectWindow()); } ); connect(c, &AbstractClient::keepAboveChanged, this, [this, c](bool above) { Q_UNUSED(above) emit windowKeepAboveChanged(c->effectWindow()); } ); connect(c, &AbstractClient::keepBelowChanged, this, [this, c](bool below) { Q_UNUSED(below) emit windowKeepBelowChanged(c->effectWindow()); } ); connect(c, &AbstractClient::fullScreenChanged, this, [this, c]() { emit windowFullScreenChanged(c->effectWindow()); } ); } void EffectsHandlerImpl::setupUnmanagedConnections(Unmanaged* u) { connect(u, &Unmanaged::windowClosed, this, &EffectsHandlerImpl::slotWindowClosed); connect(u, &Unmanaged::opacityChanged, this, &EffectsHandlerImpl::slotOpacityChanged); connect(u, &Unmanaged::geometryShapeChanged, this, &EffectsHandlerImpl::slotGeometryShapeChanged); connect(u, &Unmanaged::frameGeometryChanged, this, &EffectsHandlerImpl::slotFrameGeometryChanged); connect(u, &Unmanaged::paddingChanged, this, &EffectsHandlerImpl::slotPaddingChanged); connect(u, &Unmanaged::damaged, this, &EffectsHandlerImpl::slotWindowDamaged); } void EffectsHandlerImpl::reconfigure() { m_effectLoader->queryAndLoadAll(); } // the idea is that effects call this function again which calls the next one void EffectsHandlerImpl::prePaintScreen(ScreenPrePaintData& data, int time) { if (m_currentPaintScreenIterator != m_activeEffects.constEnd()) { (*m_currentPaintScreenIterator++)->prePaintScreen(data, time); --m_currentPaintScreenIterator; } // no special final code } void EffectsHandlerImpl::paintScreen(int mask, const QRegion ®ion, ScreenPaintData& data) { if (m_currentPaintScreenIterator != m_activeEffects.constEnd()) { (*m_currentPaintScreenIterator++)->paintScreen(mask, region, data); --m_currentPaintScreenIterator; } else m_scene->finalPaintScreen(mask, region, data); } void EffectsHandlerImpl::paintDesktop(int desktop, int mask, QRegion region, ScreenPaintData &data) { if (desktop < 1 || desktop > numberOfDesktops()) { return; } m_currentRenderedDesktop = desktop; m_desktopRendering = true; // save the paint screen iterator EffectsIterator savedIterator = m_currentPaintScreenIterator; m_currentPaintScreenIterator = m_activeEffects.constBegin(); effects->paintScreen(mask, region, data); // restore the saved iterator m_currentPaintScreenIterator = savedIterator; m_desktopRendering = false; } void EffectsHandlerImpl::postPaintScreen() { if (m_currentPaintScreenIterator != m_activeEffects.constEnd()) { (*m_currentPaintScreenIterator++)->postPaintScreen(); --m_currentPaintScreenIterator; } // no special final code } void EffectsHandlerImpl::prePaintWindow(EffectWindow* w, WindowPrePaintData& data, int time) { if (m_currentPaintWindowIterator != m_activeEffects.constEnd()) { (*m_currentPaintWindowIterator++)->prePaintWindow(w, data, time); --m_currentPaintWindowIterator; } // no special final code } void EffectsHandlerImpl::paintWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data) { if (m_currentPaintWindowIterator != m_activeEffects.constEnd()) { (*m_currentPaintWindowIterator++)->paintWindow(w, mask, region, data); --m_currentPaintWindowIterator; } else m_scene->finalPaintWindow(static_cast(w), mask, region, data); } void EffectsHandlerImpl::paintEffectFrame(EffectFrame* frame, const QRegion ®ion, double opacity, double frameOpacity) { if (m_currentPaintEffectFrameIterator != m_activeEffects.constEnd()) { (*m_currentPaintEffectFrameIterator++)->paintEffectFrame(frame, region, opacity, frameOpacity); --m_currentPaintEffectFrameIterator; } else { const EffectFrameImpl* frameImpl = static_cast(frame); frameImpl->finalRender(region, opacity, frameOpacity); } } void EffectsHandlerImpl::postPaintWindow(EffectWindow* w) { if (m_currentPaintWindowIterator != m_activeEffects.constEnd()) { (*m_currentPaintWindowIterator++)->postPaintWindow(w); --m_currentPaintWindowIterator; } // no special final code } Effect *EffectsHandlerImpl::provides(Effect::Feature ef) { for (int i = 0; i < loaded_effects.size(); ++i) if (loaded_effects.at(i).second->provides(ef)) return loaded_effects.at(i).second; return nullptr; } void EffectsHandlerImpl::drawWindow(EffectWindow* w, int mask, const QRegion ®ion, WindowPaintData& data) { if (m_currentDrawWindowIterator != m_activeEffects.constEnd()) { (*m_currentDrawWindowIterator++)->drawWindow(w, mask, region, data); --m_currentDrawWindowIterator; } else m_scene->finalDrawWindow(static_cast(w), mask, region, data); } void EffectsHandlerImpl::buildQuads(EffectWindow* w, WindowQuadList& quadList) { static bool initIterator = true; if (initIterator) { m_currentBuildQuadsIterator = m_activeEffects.constBegin(); initIterator = false; } if (m_currentBuildQuadsIterator != m_activeEffects.constEnd()) { (*m_currentBuildQuadsIterator++)->buildQuads(w, quadList); --m_currentBuildQuadsIterator; } if (m_currentBuildQuadsIterator == m_activeEffects.constBegin()) initIterator = true; } bool EffectsHandlerImpl::hasDecorationShadows() const { return false; } bool EffectsHandlerImpl::decorationsHaveAlpha() const { return true; } bool EffectsHandlerImpl::decorationSupportsBlurBehind() const { return Decoration::DecorationBridge::self()->needsBlur(); } // start another painting pass void EffectsHandlerImpl::startPaint() { m_activeEffects.clear(); m_activeEffects.reserve(loaded_effects.count()); for(QVector< KWin::EffectPair >::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { if (it->second->isActive()) { m_activeEffects << it->second; } } m_currentDrawWindowIterator = m_activeEffects.constBegin(); m_currentPaintWindowIterator = m_activeEffects.constBegin(); m_currentPaintScreenIterator = m_activeEffects.constBegin(); m_currentPaintEffectFrameIterator = m_activeEffects.constBegin(); } void EffectsHandlerImpl::slotClientMaximized(KWin::AbstractClient *c, MaximizeMode maxMode) { bool horizontal = false; bool vertical = false; switch (maxMode) { case MaximizeHorizontal: horizontal = true; break; case MaximizeVertical: vertical = true; break; case MaximizeFull: horizontal = true; vertical = true; break; case MaximizeRestore: // fall through default: // default - nothing to do break; } if (EffectWindowImpl *w = c->effectWindow()) { emit windowMaximizedStateChanged(w, horizontal, vertical); } } void EffectsHandlerImpl::slotOpacityChanged(Toplevel *t, qreal oldOpacity) { if (t->opacity() == oldOpacity || !t->effectWindow()) { return; } emit windowOpacityChanged(t->effectWindow(), oldOpacity, (qreal)t->opacity()); } void EffectsHandlerImpl::slotClientShown(KWin::Toplevel *t) { Q_ASSERT(qobject_cast(t)); AbstractClient *c = static_cast(t); disconnect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotClientShown); setupClientConnections(c); emit windowAdded(c->effectWindow()); } void EffectsHandlerImpl::slotWaylandClientShown(Toplevel *toplevel) { AbstractClient *client = static_cast(toplevel); setupClientConnections(client); emit windowAdded(toplevel->effectWindow()); } void EffectsHandlerImpl::slotUnmanagedShown(KWin::Toplevel *t) { // regardless, unmanaged windows are -yet?- not synced anyway Q_ASSERT(qobject_cast(t)); Unmanaged *u = static_cast(t); setupUnmanagedConnections(u); emit windowAdded(u->effectWindow()); } void EffectsHandlerImpl::slotWindowClosed(KWin::Toplevel *c, KWin::Deleted *d) { c->disconnect(this); if (d) { emit windowClosed(c->effectWindow()); } } void EffectsHandlerImpl::slotClientModalityChanged() { emit windowModalityChanged(static_cast(sender())->effectWindow()); } void EffectsHandlerImpl::slotCurrentTabAboutToChange(EffectWindow *from, EffectWindow *to) { emit currentTabAboutToChange(from, to); } void EffectsHandlerImpl::slotTabAdded(EffectWindow* w, EffectWindow* to) { emit tabAdded(w, to); } void EffectsHandlerImpl::slotTabRemoved(EffectWindow *w, EffectWindow* leaderOfFormerGroup) { emit tabRemoved(w, leaderOfFormerGroup); } void EffectsHandlerImpl::slotWindowDamaged(Toplevel* t, const QRect& r) { if (!t->effectWindow()) { // can happen during tear down of window return; } emit windowDamaged(t->effectWindow(), r); } void EffectsHandlerImpl::slotGeometryShapeChanged(Toplevel* t, const QRect& old) { // during late cleanup effectWindow() may be already NULL // in some functions that may still call this if (t == nullptr || t->effectWindow() == nullptr) return; emit windowGeometryShapeChanged(t->effectWindow(), old); } void EffectsHandlerImpl::slotFrameGeometryChanged(Toplevel *toplevel, const QRect &oldGeometry) { // effectWindow() might be nullptr during tear down of the client. if (toplevel->effectWindow()) { emit windowFrameGeometryChanged(toplevel->effectWindow(), oldGeometry); } } void EffectsHandlerImpl::slotPaddingChanged(Toplevel* t, const QRect& old) { // during late cleanup effectWindow() may be already NULL // in some functions that may still call this if (t == nullptr || t->effectWindow() == nullptr) return; emit windowPaddingChanged(t->effectWindow(), old); } void EffectsHandlerImpl::setActiveFullScreenEffect(Effect* e) { if (fullscreen_effect == e) { return; } const bool activeChanged = (e == nullptr || fullscreen_effect == nullptr); fullscreen_effect = e; emit activeFullScreenEffectChanged(); if (activeChanged) { emit hasActiveFullScreenEffectChanged(); } } Effect* EffectsHandlerImpl::activeFullScreenEffect() const { return fullscreen_effect; } bool EffectsHandlerImpl::hasActiveFullScreenEffect() const { return fullscreen_effect; } bool EffectsHandlerImpl::grabKeyboard(Effect* effect) { if (keyboard_grab_effect != nullptr) return false; if (!doGrabKeyboard()) { return false; } keyboard_grab_effect = effect; return true; } bool EffectsHandlerImpl::doGrabKeyboard() { return true; } void EffectsHandlerImpl::ungrabKeyboard() { Q_ASSERT(keyboard_grab_effect != nullptr); doUngrabKeyboard(); keyboard_grab_effect = nullptr; } void EffectsHandlerImpl::doUngrabKeyboard() { } void EffectsHandlerImpl::grabbedKeyboardEvent(QKeyEvent* e) { if (keyboard_grab_effect != nullptr) keyboard_grab_effect->grabbedKeyboardEvent(e); } void EffectsHandlerImpl::startMouseInterception(Effect *effect, Qt::CursorShape shape) { if (m_grabbedMouseEffects.contains(effect)) { return; } m_grabbedMouseEffects.append(effect); if (m_grabbedMouseEffects.size() != 1) { return; } doStartMouseInterception(shape); } void EffectsHandlerImpl::doStartMouseInterception(Qt::CursorShape shape) { input()->pointer()->setEffectsOverrideCursor(shape); } void EffectsHandlerImpl::stopMouseInterception(Effect *effect) { if (!m_grabbedMouseEffects.contains(effect)) { return; } m_grabbedMouseEffects.removeAll(effect); if (m_grabbedMouseEffects.isEmpty()) { doStopMouseInterception(); } } void EffectsHandlerImpl::doStopMouseInterception() { input()->pointer()->removeEffectsOverrideCursor(); } bool EffectsHandlerImpl::isMouseInterception() const { return m_grabbedMouseEffects.count() > 0; } bool EffectsHandlerImpl::touchDown(qint32 id, const QPointF &pos, quint32 time) { // TODO: reverse call order? for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { if (it->second->touchDown(id, pos, time)) { return true; } } return false; } bool EffectsHandlerImpl::touchMotion(qint32 id, const QPointF &pos, quint32 time) { // TODO: reverse call order? for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { if (it->second->touchMotion(id, pos, time)) { return true; } } return false; } bool EffectsHandlerImpl::touchUp(qint32 id, quint32 time) { // TODO: reverse call order? for (auto it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { if (it->second->touchUp(id, time)) { return true; } } return false; } void EffectsHandlerImpl::registerGlobalShortcut(const QKeySequence &shortcut, QAction *action) { input()->registerShortcut(shortcut, action); } void EffectsHandlerImpl::registerPointerShortcut(Qt::KeyboardModifiers modifiers, Qt::MouseButton pointerButtons, QAction *action) { input()->registerPointerShortcut(modifiers, pointerButtons, action); } void EffectsHandlerImpl::registerAxisShortcut(Qt::KeyboardModifiers modifiers, PointerAxisDirection axis, QAction *action) { input()->registerAxisShortcut(modifiers, axis, action); } void EffectsHandlerImpl::registerTouchpadSwipeShortcut(SwipeDirection direction, QAction *action) { input()->registerTouchpadSwipeShortcut(direction, action); } void* EffectsHandlerImpl::getProxy(QString name) { for (QVector< EffectPair >::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) if ((*it).first == name) return (*it).second->proxy(); return nullptr; } void EffectsHandlerImpl::startMousePolling() { if (Cursors::self()->mouse()) Cursors::self()->mouse()->startMousePolling(); } void EffectsHandlerImpl::stopMousePolling() { if (Cursors::self()->mouse()) Cursors::self()->mouse()->stopMousePolling(); } bool EffectsHandlerImpl::hasKeyboardGrab() const { return keyboard_grab_effect != nullptr; } void EffectsHandlerImpl::desktopResized(const QSize &size) { m_scene->screenGeometryChanged(size); emit screenGeometryChanged(size); } void EffectsHandlerImpl::registerPropertyType(long atom, bool reg) { if (reg) ++registered_atoms[ atom ]; // initialized to 0 if not present yet else { if (--registered_atoms[ atom ] == 0) registered_atoms.remove(atom); } } xcb_atom_t EffectsHandlerImpl::announceSupportProperty(const QByteArray &propertyName, Effect *effect) { PropertyEffectMap::iterator it = m_propertiesForEffects.find(propertyName); if (it != m_propertiesForEffects.end()) { // property has already been registered for an effect // just append Effect and return the atom stored in m_managedProperties if (!it.value().contains(effect)) { it.value().append(effect); } return m_managedProperties.value(propertyName, XCB_ATOM_NONE); } m_propertiesForEffects.insert(propertyName, QList() << effect); const auto atom = registerSupportProperty(propertyName); if (atom == XCB_ATOM_NONE) { return atom; } m_compositor->keepSupportProperty(atom); m_managedProperties.insert(propertyName, atom); registerPropertyType(atom, true); return atom; } void EffectsHandlerImpl::removeSupportProperty(const QByteArray &propertyName, Effect *effect) { PropertyEffectMap::iterator it = m_propertiesForEffects.find(propertyName); if (it == m_propertiesForEffects.end()) { // property is not registered - nothing to do return; } if (!it.value().contains(effect)) { // property is not registered for given effect - nothing to do return; } it.value().removeAll(effect); if (!it.value().isEmpty()) { // property still registered for another effect - nothing further to do return; } const xcb_atom_t atom = m_managedProperties.take(propertyName); registerPropertyType(atom, false); m_propertiesForEffects.remove(propertyName); m_compositor->removeSupportProperty(atom); // delayed removal } QByteArray EffectsHandlerImpl::readRootProperty(long atom, long type, int format) const { if (!kwinApp()->x11Connection()) { return QByteArray(); } return readWindowProperty(kwinApp()->x11RootWindow(), atom, type, format); } void EffectsHandlerImpl::activateWindow(EffectWindow* c) { if (auto cl = qobject_cast(static_cast(c)->window())) { Workspace::self()->activateClient(cl, true); } } EffectWindow* EffectsHandlerImpl::activeWindow() const { return Workspace::self()->activeClient() ? Workspace::self()->activeClient()->effectWindow() : nullptr; } void EffectsHandlerImpl::moveWindow(EffectWindow* w, const QPoint& pos, bool snap, double snapAdjust) { auto cl = qobject_cast(static_cast(w)->window()); if (!cl || !cl->isMovable()) return; if (snap) cl->move(Workspace::self()->adjustClientPosition(cl, pos, true, snapAdjust)); else cl->move(pos); } void EffectsHandlerImpl::windowToDesktop(EffectWindow* w, int desktop) { auto cl = qobject_cast(static_cast(w)->window()); if (cl && !cl->isDesktop() && !cl->isDock()) { Workspace::self()->sendClientToDesktop(cl, desktop, true); } } void EffectsHandlerImpl::windowToDesktops(EffectWindow *w, const QVector &desktopIds) { AbstractClient* cl = qobject_cast< AbstractClient* >(static_cast(w)->window()); if (!cl || cl->isDesktop() || cl->isDock()) { return; } QVector desktops; desktops.reserve(desktopIds.count()); for (uint x11Id: desktopIds) { if (x11Id > VirtualDesktopManager::self()->count()) { continue; } VirtualDesktop *d = VirtualDesktopManager::self()->desktopForX11Id(x11Id); Q_ASSERT(d); if (desktops.contains(d)) { continue; } desktops << d; } cl->setDesktops(desktops); } void EffectsHandlerImpl::windowToScreen(EffectWindow* w, int screen) { auto cl = qobject_cast(static_cast(w)->window()); if (cl && !cl->isDesktop() && !cl->isDock()) Workspace::self()->sendClientToScreen(cl, screen); } void EffectsHandlerImpl::setShowingDesktop(bool showing) { Workspace::self()->setShowingDesktop(showing); } QString EffectsHandlerImpl::currentActivity() const { #ifdef KWIN_BUILD_ACTIVITIES if (!Activities::self()) { return QString(); } return Activities::self()->current(); #else return QString(); #endif } int EffectsHandlerImpl::currentDesktop() const { return VirtualDesktopManager::self()->current(); } int EffectsHandlerImpl::numberOfDesktops() const { return VirtualDesktopManager::self()->count(); } void EffectsHandlerImpl::setCurrentDesktop(int desktop) { VirtualDesktopManager::self()->setCurrent(desktop); } void EffectsHandlerImpl::setNumberOfDesktops(int desktops) { VirtualDesktopManager::self()->setCount(desktops); } QSize EffectsHandlerImpl::desktopGridSize() const { return VirtualDesktopManager::self()->grid().size(); } int EffectsHandlerImpl::desktopGridWidth() const { return desktopGridSize().width(); } int EffectsHandlerImpl::desktopGridHeight() const { return desktopGridSize().height(); } int EffectsHandlerImpl::workspaceWidth() const { return desktopGridWidth() * screens()->size().width(); } int EffectsHandlerImpl::workspaceHeight() const { return desktopGridHeight() * screens()->size().height(); } int EffectsHandlerImpl::desktopAtCoords(QPoint coords) const { if (auto vd = VirtualDesktopManager::self()->grid().at(coords)) { return vd->x11DesktopNumber(); } return 0; } QPoint EffectsHandlerImpl::desktopGridCoords(int id) const { return VirtualDesktopManager::self()->grid().gridCoords(id); } QPoint EffectsHandlerImpl::desktopCoords(int id) const { QPoint coords = VirtualDesktopManager::self()->grid().gridCoords(id); if (coords.x() == -1) return QPoint(-1, -1); const QSize displaySize = screens()->size(); return QPoint(coords.x() * displaySize.width(), coords.y() * displaySize.height()); } int EffectsHandlerImpl::desktopAbove(int desktop, bool wrap) const { return getDesktop(desktop, wrap); } int EffectsHandlerImpl::desktopToRight(int desktop, bool wrap) const { return getDesktop(desktop, wrap); } int EffectsHandlerImpl::desktopBelow(int desktop, bool wrap) const { return getDesktop(desktop, wrap); } int EffectsHandlerImpl::desktopToLeft(int desktop, bool wrap) const { return getDesktop(desktop, wrap); } QString EffectsHandlerImpl::desktopName(int desktop) const { return VirtualDesktopManager::self()->name(desktop); } bool EffectsHandlerImpl::optionRollOverDesktops() const { return options->isRollOverDesktops(); } double EffectsHandlerImpl::animationTimeFactor() const { return options->animationTimeFactor(); } WindowQuadType EffectsHandlerImpl::newWindowQuadType() { return WindowQuadType(next_window_quad_type++); } EffectWindow* EffectsHandlerImpl::findWindow(WId id) const { if (X11Client *w = Workspace::self()->findClient(Predicate::WindowMatch, id)) return w->effectWindow(); if (Unmanaged* w = Workspace::self()->findUnmanaged(id)) return w->effectWindow(); if (waylandServer()) { if (AbstractClient *w = waylandServer()->findClient(id)) { return w->effectWindow(); } } return nullptr; } EffectWindow* EffectsHandlerImpl::findWindow(KWaylandServer::SurfaceInterface *surf) const { if (waylandServer()) { if (AbstractClient *w = waylandServer()->findClient(surf)) { return w->effectWindow(); } } return nullptr; } EffectWindow *EffectsHandlerImpl::findWindow(QWindow *w) const { if (Toplevel *toplevel = workspace()->findInternal(w)) { return toplevel->effectWindow(); } return nullptr; } EffectWindow *EffectsHandlerImpl::findWindow(const QUuid &id) const { if (const auto client = workspace()->findAbstractClient([&id] (const AbstractClient *c) { return c->internalId() == id; })) { return client->effectWindow(); } if (const auto unmanaged = workspace()->findUnmanaged([&id] (const Unmanaged *c) { return c->internalId() == id; })) { return unmanaged->effectWindow(); } return nullptr; } EffectWindowList EffectsHandlerImpl::stackingOrder() const { QList list = Workspace::self()->xStackingOrder(); EffectWindowList ret; for (Toplevel *t : list) { if (EffectWindow *w = effectWindow(t)) ret.append(w); } return ret; } void EffectsHandlerImpl::setElevatedWindow(KWin::EffectWindow* w, bool set) { elevated_windows.removeAll(w); if (set) elevated_windows.append(w); } void EffectsHandlerImpl::setTabBoxWindow(EffectWindow* w) { #ifdef KWIN_BUILD_TABBOX if (auto c = qobject_cast(static_cast(w)->window())) { TabBox::TabBox::self()->setCurrentClient(c); } #else Q_UNUSED(w) #endif } void EffectsHandlerImpl::setTabBoxDesktop(int desktop) { #ifdef KWIN_BUILD_TABBOX TabBox::TabBox::self()->setCurrentDesktop(desktop); #else Q_UNUSED(desktop) #endif } EffectWindowList EffectsHandlerImpl::currentTabBoxWindowList() const { #ifdef KWIN_BUILD_TABBOX const auto clients = TabBox::TabBox::self()->currentClientList(); EffectWindowList ret; ret.reserve(clients.size()); std::transform(std::cbegin(clients), std::cend(clients), std::back_inserter(ret), [](auto client) { return client->effectWindow(); }); return ret; #else return EffectWindowList(); #endif } void EffectsHandlerImpl::refTabBox() { #ifdef KWIN_BUILD_TABBOX TabBox::TabBox::self()->reference(); #endif } void EffectsHandlerImpl::unrefTabBox() { #ifdef KWIN_BUILD_TABBOX TabBox::TabBox::self()->unreference(); #endif } void EffectsHandlerImpl::closeTabBox() { #ifdef KWIN_BUILD_TABBOX TabBox::TabBox::self()->close(); #endif } QList< int > EffectsHandlerImpl::currentTabBoxDesktopList() const { #ifdef KWIN_BUILD_TABBOX return TabBox::TabBox::self()->currentDesktopList(); #else return QList< int >(); #endif } int EffectsHandlerImpl::currentTabBoxDesktop() const { #ifdef KWIN_BUILD_TABBOX return TabBox::TabBox::self()->currentDesktop(); #else return -1; #endif } EffectWindow* EffectsHandlerImpl::currentTabBoxWindow() const { #ifdef KWIN_BUILD_TABBOX if (auto c = TabBox::TabBox::self()->currentClient()) return c->effectWindow(); #endif return nullptr; } void EffectsHandlerImpl::addRepaintFull() { m_compositor->addRepaintFull(); } void EffectsHandlerImpl::addRepaint(const QRect& r) { m_compositor->addRepaint(r); } void EffectsHandlerImpl::addRepaint(const QRegion& r) { m_compositor->addRepaint(r); } void EffectsHandlerImpl::addRepaint(int x, int y, int w, int h) { m_compositor->addRepaint(x, y, w, h); } int EffectsHandlerImpl::activeScreen() const { return screens()->current(); } int EffectsHandlerImpl::numScreens() const { return screens()->count(); } int EffectsHandlerImpl::screenNumber(const QPoint& pos) const { return screens()->number(pos); } QRect EffectsHandlerImpl::clientArea(clientAreaOption opt, int screen, int desktop) const { return Workspace::self()->clientArea(opt, screen, desktop); } QRect EffectsHandlerImpl::clientArea(clientAreaOption opt, const EffectWindow* c) const { const Toplevel* t = static_cast< const EffectWindowImpl* >(c)->window(); if (const auto *cl = qobject_cast(t)) { return Workspace::self()->clientArea(opt, cl); } else { return Workspace::self()->clientArea(opt, t->frameGeometry().center(), VirtualDesktopManager::self()->current()); } } QRect EffectsHandlerImpl::clientArea(clientAreaOption opt, const QPoint& p, int desktop) const { return Workspace::self()->clientArea(opt, p, desktop); } QRect EffectsHandlerImpl::virtualScreenGeometry() const { return screens()->geometry(); } QSize EffectsHandlerImpl::virtualScreenSize() const { return screens()->size(); } void EffectsHandlerImpl::defineCursor(Qt::CursorShape shape) { input()->pointer()->setEffectsOverrideCursor(shape); } bool EffectsHandlerImpl::checkInputWindowEvent(QMouseEvent *e) { if (m_grabbedMouseEffects.isEmpty()) { return false; } foreach (Effect *effect, m_grabbedMouseEffects) { effect->windowInputMouseEvent(e); } return true; } bool EffectsHandlerImpl::checkInputWindowEvent(QWheelEvent *e) { if (m_grabbedMouseEffects.isEmpty()) { return false; } foreach (Effect *effect, m_grabbedMouseEffects) { effect->windowInputMouseEvent(e); } return true; } void EffectsHandlerImpl::connectNotify(const QMetaMethod &signal) { if (signal == QMetaMethod::fromSignal(&EffectsHandler::cursorShapeChanged)) { if (!m_trackingCursorChanges) { connect(Cursors::self()->mouse(), &Cursor::cursorChanged, this, &EffectsHandler::cursorShapeChanged); Cursors::self()->mouse()->startCursorTracking(); } ++m_trackingCursorChanges; } EffectsHandler::connectNotify(signal); } void EffectsHandlerImpl::disconnectNotify(const QMetaMethod &signal) { if (signal == QMetaMethod::fromSignal(&EffectsHandler::cursorShapeChanged)) { Q_ASSERT(m_trackingCursorChanges > 0); if (!--m_trackingCursorChanges) { Cursors::self()->mouse()->stopCursorTracking(); disconnect(Cursors::self()->mouse(), &Cursor::cursorChanged, this, &EffectsHandler::cursorShapeChanged); } } EffectsHandler::disconnectNotify(signal); } void EffectsHandlerImpl::checkInputWindowStacking() { if (m_grabbedMouseEffects.isEmpty()) { return; } doCheckInputWindowStacking(); } void EffectsHandlerImpl::doCheckInputWindowStacking() { } QPoint EffectsHandlerImpl::cursorPos() const { return Cursors::self()->mouse()->pos(); } void EffectsHandlerImpl::reserveElectricBorder(ElectricBorder border, Effect *effect) { ScreenEdges::self()->reserve(border, effect, "borderActivated"); } void EffectsHandlerImpl::unreserveElectricBorder(ElectricBorder border, Effect *effect) { ScreenEdges::self()->unreserve(border, effect); } void EffectsHandlerImpl::registerTouchBorder(ElectricBorder border, QAction *action) { ScreenEdges::self()->reserveTouch(border, action); } void EffectsHandlerImpl::unregisterTouchBorder(ElectricBorder border, QAction *action) { ScreenEdges::self()->unreserveTouch(border, action); } unsigned long EffectsHandlerImpl::xrenderBufferPicture() { return m_scene->xrenderBufferPicture(); } QPainter *EffectsHandlerImpl::scenePainter() { return m_scene->scenePainter(); } void EffectsHandlerImpl::toggleEffect(const QString& name) { if (isEffectLoaded(name)) unloadEffect(name); else loadEffect(name); } QStringList EffectsHandlerImpl::loadedEffects() const { QStringList listModules; listModules.reserve(loaded_effects.count()); std::transform(loaded_effects.constBegin(), loaded_effects.constEnd(), std::back_inserter(listModules), [](const EffectPair &pair) { return pair.first; }); return listModules; } QStringList EffectsHandlerImpl::listOfEffects() const { return m_effectLoader->listOfKnownEffects(); } bool EffectsHandlerImpl::loadEffect(const QString& name) { makeOpenGLContextCurrent(); m_compositor->addRepaintFull(); return m_effectLoader->loadEffect(name); } void EffectsHandlerImpl::unloadEffect(const QString& name) { auto it = std::find_if(effect_order.begin(), effect_order.end(), [name](EffectPair &pair) { return pair.first == name; } ); if (it == effect_order.end()) { qCDebug(KWIN_CORE) << "EffectsHandler::unloadEffect : Effect not loaded :" << name; return; } qCDebug(KWIN_CORE) << "EffectsHandler::unloadEffect : Unloading Effect :" << name; destroyEffect((*it).second); effect_order.erase(it); effectsChanged(); m_compositor->addRepaintFull(); } void EffectsHandlerImpl::destroyEffect(Effect *effect) { makeOpenGLContextCurrent(); if (fullscreen_effect == effect) { setActiveFullScreenEffect(nullptr); } if (keyboard_grab_effect == effect) { ungrabKeyboard(); } stopMouseInterception(effect); const QList properties = m_propertiesForEffects.keys(); for (const QByteArray &property : properties) { removeSupportProperty(property, effect); } delete effect; } void EffectsHandlerImpl::reconfigureEffect(const QString& name) { for (QVector< EffectPair >::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) if ((*it).first == name) { kwinApp()->config()->reparseConfiguration(); makeOpenGLContextCurrent(); (*it).second->reconfigure(Effect::ReconfigureAll); return; } } bool EffectsHandlerImpl::isEffectLoaded(const QString& name) const { auto it = std::find_if(loaded_effects.constBegin(), loaded_effects.constEnd(), [&name](const EffectPair &pair) { return pair.first == name; }); return it != loaded_effects.constEnd(); } bool EffectsHandlerImpl::isEffectSupported(const QString &name) { // If the effect is loaded, it is obviously supported. if (isEffectLoaded(name)) { return true; } // next checks might require a context makeOpenGLContextCurrent(); m_compositor->addRepaintFull(); return m_effectLoader->isEffectSupported(name); } QList EffectsHandlerImpl::areEffectsSupported(const QStringList &names) { QList retList; retList.reserve(names.count()); std::transform(names.constBegin(), names.constEnd(), std::back_inserter(retList), [this](const QString &name) { return isEffectSupported(name); }); return retList; } void EffectsHandlerImpl::reloadEffect(Effect *effect) { QString effectName; for (QVector< EffectPair >::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { if ((*it).second == effect) { effectName = (*it).first; break; } } if (!effectName.isNull()) { unloadEffect(effectName); m_effectLoader->loadEffect(effectName); } } void EffectsHandlerImpl::effectsChanged() { loaded_effects.clear(); m_activeEffects.clear(); // it's possible to have a reconfigure and a quad rebuild between two paint cycles - bug #308201 loaded_effects.reserve(effect_order.count()); std::copy(effect_order.constBegin(), effect_order.constEnd(), std::back_inserter(loaded_effects)); m_activeEffects.reserve(loaded_effects.count()); } QStringList EffectsHandlerImpl::activeEffects() const { QStringList ret; for(QVector< KWin::EffectPair >::const_iterator it = loaded_effects.constBegin(), end = loaded_effects.constEnd(); it != end; ++it) { if (it->second->isActive()) { ret << it->first; } } return ret; } KWaylandServer::Display *EffectsHandlerImpl::waylandDisplay() const { if (waylandServer()) { return waylandServer()->display(); } return nullptr; } EffectFrame* EffectsHandlerImpl::effectFrame(EffectFrameStyle style, bool staticSize, const QPoint& position, Qt::Alignment alignment) const { return new EffectFrameImpl(style, staticSize, position, alignment); } QVariant EffectsHandlerImpl::kwinOption(KWinOption kwopt) { switch (kwopt) { case CloseButtonCorner: // TODO: this could become per window and be derived from the actual position in the deco return Decoration::DecorationBridge::self()->settings()->decorationButtonsLeft().contains(KDecoration2::DecorationButtonType::Close) ? Qt::TopLeftCorner : Qt::TopRightCorner; case SwitchDesktopOnScreenEdge: return ScreenEdges::self()->isDesktopSwitching(); case SwitchDesktopOnScreenEdgeMovingWindows: return ScreenEdges::self()->isDesktopSwitchingMovingClients(); default: return QVariant(); // an invalid one } } QString EffectsHandlerImpl::supportInformation(const QString &name) const { auto it = std::find_if(loaded_effects.constBegin(), loaded_effects.constEnd(), [name](const EffectPair &pair) { return pair.first == name; }); if (it == loaded_effects.constEnd()) { return QString(); } QString support((*it).first + QLatin1String(":\n")); const QMetaObject *metaOptions = (*it).second->metaObject(); for (int i=0; ipropertyCount(); ++i) { const QMetaProperty property = metaOptions->property(i); if (qstrcmp(property.name(), "objectName") == 0) { continue; } support += QString::fromUtf8(property.name()) + QLatin1String(": ") + (*it).second->property(property.name()).toString() + QLatin1Char('\n'); } return support; } bool EffectsHandlerImpl::isScreenLocked() const { return ScreenLockerWatcher::self()->isLocked(); } QString EffectsHandlerImpl::debug(const QString& name, const QString& parameter) const { QString internalName = name.toLower();; for (QVector< EffectPair >::const_iterator it = loaded_effects.constBegin(); it != loaded_effects.constEnd(); ++it) { if ((*it).first == internalName) { return it->second->debug(parameter); } } return QString(); } bool EffectsHandlerImpl::makeOpenGLContextCurrent() { return m_scene->makeOpenGLContextCurrent(); } void EffectsHandlerImpl::doneOpenGLContextCurrent() { m_scene->doneOpenGLContextCurrent(); } bool EffectsHandlerImpl::animationsSupported() const { static const QByteArray forceEnvVar = qgetenv("KWIN_EFFECTS_FORCE_ANIMATIONS"); if (!forceEnvVar.isEmpty()) { static const int forceValue = forceEnvVar.toInt(); return forceValue == 1; } return m_scene->animationsSupported(); } void EffectsHandlerImpl::highlightWindows(const QVector &windows) { Effect *e = provides(Effect::HighlightWindows); if (!e) { return; } e->perform(Effect::HighlightWindows, QVariantList{QVariant::fromValue(windows)}); } PlatformCursorImage EffectsHandlerImpl::cursorImage() const { return kwinApp()->platform()->cursorImage(); } void EffectsHandlerImpl::hideCursor() { kwinApp()->platform()->hideCursor(); } void EffectsHandlerImpl::showCursor() { kwinApp()->platform()->showCursor(); } void EffectsHandlerImpl::startInteractiveWindowSelection(std::function callback) { kwinApp()->platform()->startInteractiveWindowSelection( [callback] (KWin::Toplevel *t) { if (t && t->effectWindow()) { callback(t->effectWindow()); } else { callback(nullptr); } } ); } void EffectsHandlerImpl::startInteractivePositionSelection(std::function callback) { kwinApp()->platform()->startInteractivePositionSelection(callback); } void EffectsHandlerImpl::showOnScreenMessage(const QString &message, const QString &iconName) { OSD::show(message, iconName); } void EffectsHandlerImpl::hideOnScreenMessage(OnScreenMessageHideFlags flags) { OSD::HideFlags osdFlags; if (flags.testFlag(OnScreenMessageHideFlag::SkipsCloseAnimation)) { osdFlags |= OSD::HideFlag::SkipCloseAnimation; } OSD::hide(osdFlags); } KSharedConfigPtr EffectsHandlerImpl::config() const { return kwinApp()->config(); } KSharedConfigPtr EffectsHandlerImpl::inputConfig() const { return kwinApp()->inputConfig(); } Effect *EffectsHandlerImpl::findEffect(const QString &name) const { auto it = std::find_if(loaded_effects.constBegin(), loaded_effects.constEnd(), [name] (const EffectPair &pair) { return pair.first == name; } ); if (it == loaded_effects.constEnd()) { return nullptr; } return (*it).second; } void EffectsHandlerImpl::renderEffectQuickView(EffectQuickView *w) const { if (!w->isVisible()) { return; } scene()->paintEffectQuickView(w); } SessionState EffectsHandlerImpl::sessionState() const { return Workspace::self()->sessionManager()->state(); } //**************************************** // EffectWindowImpl //**************************************** EffectWindowImpl::EffectWindowImpl(Toplevel *toplevel) : EffectWindow(toplevel) , toplevel(toplevel) , sw(nullptr) { // Deleted windows are not managed. So, when windowClosed signal is // emitted, effects can't distinguish managed windows from unmanaged // windows(e.g. combo box popups, popup menus, etc). Save value of the // managed property during construction of EffectWindow. At that time, // parent can be Client, XdgShellClient, or Unmanaged. So, later on, when // an instance of Deleted becomes parent of the EffectWindow, effects // can still figure out whether it is/was a managed window. managed = toplevel->isClient(); - waylandClient = qobject_cast(toplevel) != nullptr; + waylandClient = qobject_cast(toplevel) != nullptr; x11Client = qobject_cast(toplevel) != nullptr || qobject_cast(toplevel) != nullptr; } EffectWindowImpl::~EffectWindowImpl() { QVariant cachedTextureVariant = data(LanczosCacheRole); if (cachedTextureVariant.isValid()) { GLTexture *cachedTexture = static_cast< GLTexture*>(cachedTextureVariant.value()); delete cachedTexture; } } bool EffectWindowImpl::isPaintingEnabled() { return sceneWindow()->isPaintingEnabled(); } void EffectWindowImpl::enablePainting(int reason) { sceneWindow()->enablePainting(reason); } void EffectWindowImpl::disablePainting(int reason) { sceneWindow()->disablePainting(reason); } void EffectWindowImpl::addRepaint(const QRect &r) { toplevel->addRepaint(r); } void EffectWindowImpl::addRepaint(int x, int y, int w, int h) { toplevel->addRepaint(x, y, w, h); } void EffectWindowImpl::addRepaintFull() { toplevel->addRepaintFull(); } void EffectWindowImpl::addLayerRepaint(const QRect &r) { toplevel->addLayerRepaint(r); } void EffectWindowImpl::addLayerRepaint(int x, int y, int w, int h) { toplevel->addLayerRepaint(x, y, w, h); } const EffectWindowGroup* EffectWindowImpl::group() const { if (auto c = qobject_cast(toplevel)) { return c->group()->effectGroup(); } return nullptr; // TODO } void EffectWindowImpl::refWindow() { if (auto d = qobject_cast(toplevel)) { return d->refWindow(); } abort(); // TODO } void EffectWindowImpl::unrefWindow() { if (auto d = qobject_cast(toplevel)) { return d->unrefWindow(); // delays deletion in case } abort(); // TODO } #define TOPLEVEL_HELPER( rettype, prototype, toplevelPrototype) \ rettype EffectWindowImpl::prototype ( ) const \ { \ return toplevel->toplevelPrototype(); \ } TOPLEVEL_HELPER(double, opacity, opacity) TOPLEVEL_HELPER(bool, hasAlpha, hasAlpha) TOPLEVEL_HELPER(int, x, x) TOPLEVEL_HELPER(int, y, y) TOPLEVEL_HELPER(int, width, width) TOPLEVEL_HELPER(int, height, height) TOPLEVEL_HELPER(QPoint, pos, pos) TOPLEVEL_HELPER(QSize, size, size) TOPLEVEL_HELPER(int, screen, screen) TOPLEVEL_HELPER(QRect, geometry, frameGeometry) TOPLEVEL_HELPER(QRect, frameGeometry, frameGeometry) TOPLEVEL_HELPER(QRect, bufferGeometry, bufferGeometry) TOPLEVEL_HELPER(QRect, expandedGeometry, visibleRect) TOPLEVEL_HELPER(QRect, rect, rect) TOPLEVEL_HELPER(int, desktop, desktop) TOPLEVEL_HELPER(bool, isDesktop, isDesktop) TOPLEVEL_HELPER(bool, isDock, isDock) TOPLEVEL_HELPER(bool, isToolbar, isToolbar) TOPLEVEL_HELPER(bool, isMenu, isMenu) TOPLEVEL_HELPER(bool, isNormalWindow, isNormalWindow) TOPLEVEL_HELPER(bool, isDialog, isDialog) TOPLEVEL_HELPER(bool, isSplash, isSplash) TOPLEVEL_HELPER(bool, isUtility, isUtility) TOPLEVEL_HELPER(bool, isDropdownMenu, isDropdownMenu) TOPLEVEL_HELPER(bool, isPopupMenu, isPopupMenu) TOPLEVEL_HELPER(bool, isTooltip, isTooltip) TOPLEVEL_HELPER(bool, isNotification, isNotification) TOPLEVEL_HELPER(bool, isCriticalNotification, isCriticalNotification) TOPLEVEL_HELPER(bool, isOnScreenDisplay, isOnScreenDisplay) TOPLEVEL_HELPER(bool, isComboBox, isComboBox) TOPLEVEL_HELPER(bool, isDNDIcon, isDNDIcon) TOPLEVEL_HELPER(bool, isDeleted, isDeleted) TOPLEVEL_HELPER(bool, hasOwnShape, shape) TOPLEVEL_HELPER(QString, windowRole, windowRole) TOPLEVEL_HELPER(QStringList, activities, activities) TOPLEVEL_HELPER(bool, skipsCloseAnimation, skipsCloseAnimation) TOPLEVEL_HELPER(KWaylandServer::SurfaceInterface *, surface, surface) TOPLEVEL_HELPER(bool, isPopupWindow, isPopupWindow) TOPLEVEL_HELPER(bool, isOutline, isOutline) TOPLEVEL_HELPER(pid_t, pid, pid) #undef TOPLEVEL_HELPER #define CLIENT_HELPER_WITH_DELETED( rettype, prototype, propertyname, defaultValue ) \ rettype EffectWindowImpl::prototype ( ) const \ { \ auto client = qobject_cast(toplevel); \ if (client) { \ return client->propertyname(); \ } \ auto deleted = qobject_cast(toplevel); \ if (deleted) { \ return deleted->propertyname(); \ } \ return defaultValue; \ } CLIENT_HELPER_WITH_DELETED(bool, isMinimized, isMinimized, false) CLIENT_HELPER_WITH_DELETED(bool, isModal, isModal, false) CLIENT_HELPER_WITH_DELETED(bool, isFullScreen, isFullScreen, false) CLIENT_HELPER_WITH_DELETED(bool, keepAbove, keepAbove, false) CLIENT_HELPER_WITH_DELETED(bool, keepBelow, keepBelow, false) CLIENT_HELPER_WITH_DELETED(QString, caption, caption, QString()); CLIENT_HELPER_WITH_DELETED(QVector, desktops, x11DesktopIds, QVector()); #undef CLIENT_HELPER_WITH_DELETED // legacy from tab groups, can be removed when no effects use this any more. bool EffectWindowImpl::isCurrentTab() const { return true; } QString EffectWindowImpl::windowClass() const { return toplevel->resourceName() + QLatin1Char(' ') + toplevel->resourceClass(); } QRect EffectWindowImpl::contentsRect() const { return QRect(toplevel->clientPos(), toplevel->clientSize()); } NET::WindowType EffectWindowImpl::windowType() const { return toplevel->windowType(); } #define CLIENT_HELPER( rettype, prototype, propertyname, defaultValue ) \ rettype EffectWindowImpl::prototype ( ) const \ { \ auto client = qobject_cast(toplevel); \ if (client) { \ return client->propertyname(); \ } \ return defaultValue; \ } CLIENT_HELPER(bool, isMovable, isMovable, false) CLIENT_HELPER(bool, isMovableAcrossScreens, isMovableAcrossScreens, false) CLIENT_HELPER(bool, isUserMove, isMove, false) CLIENT_HELPER(bool, isUserResize, isResize, false) CLIENT_HELPER(QRect, iconGeometry, iconGeometry, QRect()) CLIENT_HELPER(bool, isSpecialWindow, isSpecialWindow, true) CLIENT_HELPER(bool, acceptsFocus, wantsInput, true) // We don't actually know... CLIENT_HELPER(QIcon, icon, icon, QIcon()) CLIENT_HELPER(bool, isSkipSwitcher, skipSwitcher, false) CLIENT_HELPER(bool, decorationHasAlpha, decorationHasAlpha, false) CLIENT_HELPER(bool, isUnresponsive, unresponsive, false) #undef CLIENT_HELPER QSize EffectWindowImpl::basicUnit() const { if (auto client = qobject_cast(toplevel)){ return client->basicUnit(); } return QSize(1,1); } void EffectWindowImpl::setWindow(Toplevel* w) { toplevel = w; setParent(w); } void EffectWindowImpl::setSceneWindow(Scene::Window* w) { sw = w; } QRegion EffectWindowImpl::shape() const { if (isX11Client() && sceneWindow()) { return sceneWindow()->bufferShape(); } return toplevel->rect(); } QRect EffectWindowImpl::decorationInnerRect() const { auto client = qobject_cast(toplevel); return client ? client->transparentRect() : contentsRect(); } QByteArray EffectWindowImpl::readProperty(long atom, long type, int format) const { if (!kwinApp()->x11Connection()) { return QByteArray(); } return readWindowProperty(window()->window(), atom, type, format); } void EffectWindowImpl::deleteProperty(long int atom) const { if (kwinApp()->x11Connection()) { deleteWindowProperty(window()->window(), atom); } } EffectWindow* EffectWindowImpl::findModal() { auto client = qobject_cast(toplevel); if (!client) { return nullptr; } AbstractClient *modal = client->findModal(); if (modal) { return modal->effectWindow(); } return nullptr; } QWindow *EffectWindowImpl::internalWindow() const { auto client = qobject_cast(toplevel); if (!client) { return nullptr; } return client->internalWindow(); } template EffectWindowList getMainWindows(T *c) { const auto mainclients = c->mainClients(); EffectWindowList ret; ret.reserve(mainclients.size()); std::transform(std::cbegin(mainclients), std::cend(mainclients), std::back_inserter(ret), [](auto client) { return client->effectWindow(); }); return ret; } EffectWindowList EffectWindowImpl::mainWindows() const { if (auto client = qobject_cast(toplevel)) { return getMainWindows(client); } if (auto deleted = qobject_cast(toplevel)) { return getMainWindows(deleted); } return {}; } WindowQuadList EffectWindowImpl::buildQuads(bool force) const { return sceneWindow()->buildQuads(force); } void EffectWindowImpl::setData(int role, const QVariant &data) { if (!data.isNull()) dataMap[ role ] = data; else dataMap.remove(role); emit effects->windowDataChanged(this, role); } QVariant EffectWindowImpl::data(int role) const { return dataMap.value(role); } EffectWindow* effectWindow(Toplevel* w) { EffectWindowImpl* ret = w->effectWindow(); return ret; } EffectWindow* effectWindow(Scene::Window* w) { EffectWindowImpl* ret = w->window()->effectWindow(); ret->setSceneWindow(w); return ret; } void EffectWindowImpl::elevate(bool elevate) { effects->setElevatedWindow(this, elevate); } void EffectWindowImpl::registerThumbnail(AbstractThumbnailItem *item) { if (WindowThumbnailItem *thumb = qobject_cast(item)) { insertThumbnail(thumb); connect(thumb, SIGNAL(destroyed(QObject*)), SLOT(thumbnailDestroyed(QObject*))); connect(thumb, &WindowThumbnailItem::wIdChanged, this, &EffectWindowImpl::thumbnailTargetChanged); } else if (DesktopThumbnailItem *desktopThumb = qobject_cast(item)) { m_desktopThumbnails.append(desktopThumb); connect(desktopThumb, SIGNAL(destroyed(QObject*)), SLOT(desktopThumbnailDestroyed(QObject*))); } } void EffectWindowImpl::thumbnailDestroyed(QObject *object) { // we know it is a ThumbnailItem m_thumbnails.remove(static_cast(object)); } void EffectWindowImpl::thumbnailTargetChanged() { if (WindowThumbnailItem *item = qobject_cast(sender())) { insertThumbnail(item); } } void EffectWindowImpl::insertThumbnail(WindowThumbnailItem *item) { EffectWindow *w = effects->findWindow(item->wId()); if (w) { m_thumbnails.insert(item, QPointer(static_cast(w))); } else { m_thumbnails.insert(item, QPointer()); } } void EffectWindowImpl::desktopThumbnailDestroyed(QObject *object) { // we know it is a DesktopThumbnailItem m_desktopThumbnails.removeAll(static_cast(object)); } void EffectWindowImpl::minimize() { if (auto client = qobject_cast(toplevel)) { client->minimize(); } } void EffectWindowImpl::unminimize() { if (auto client = qobject_cast(toplevel)) { client->unminimize(); } } void EffectWindowImpl::closeWindow() { if (auto client = qobject_cast(toplevel)) { client->closeWindow(); } } void EffectWindowImpl::referencePreviousWindowPixmap() { if (sw) { sw->referencePreviousPixmap(); } } void EffectWindowImpl::unreferencePreviousWindowPixmap() { if (sw) { sw->unreferencePreviousPixmap(); } } bool EffectWindowImpl::isManaged() const { return managed; } bool EffectWindowImpl::isWaylandClient() const { return waylandClient; } bool EffectWindowImpl::isX11Client() const { return x11Client; } //**************************************** // EffectWindowGroupImpl //**************************************** EffectWindowList EffectWindowGroupImpl::members() const { const auto memberList = group->members(); EffectWindowList ret; ret.reserve(memberList.size()); std::transform(std::cbegin(memberList), std::cend(memberList), std::back_inserter(ret), [](auto toplevel) { return toplevel->effectWindow(); }); return ret; } //**************************************** // EffectFrameImpl //**************************************** EffectFrameImpl::EffectFrameImpl(EffectFrameStyle style, bool staticSize, QPoint position, Qt::Alignment alignment) : QObject(nullptr) , EffectFrame() , m_style(style) , m_static(staticSize) , m_point(position) , m_alignment(alignment) , m_shader(nullptr) , m_theme(new Plasma::Theme(this)) { if (m_style == EffectFrameStyled) { m_frame.setImagePath(QStringLiteral("widgets/background")); m_frame.setCacheAllRenderedFrames(true); connect(m_theme, SIGNAL(themeChanged()), this, SLOT(plasmaThemeChanged())); } m_selection.setImagePath(QStringLiteral("widgets/viewitem")); m_selection.setElementPrefix(QStringLiteral("hover")); m_selection.setCacheAllRenderedFrames(true); m_selection.setEnabledBorders(Plasma::FrameSvg::AllBorders); m_sceneFrame = Compositor::self()->scene()->createEffectFrame(this); } EffectFrameImpl::~EffectFrameImpl() { delete m_sceneFrame; } const QFont& EffectFrameImpl::font() const { return m_font; } void EffectFrameImpl::setFont(const QFont& font) { if (m_font == font) { return; } m_font = font; QRect oldGeom = m_geometry; if (!m_text.isEmpty()) { autoResize(); } if (oldGeom == m_geometry) { // Wasn't updated in autoResize() m_sceneFrame->freeTextFrame(); } } void EffectFrameImpl::free() { m_sceneFrame->free(); } const QRect& EffectFrameImpl::geometry() const { return m_geometry; } void EffectFrameImpl::setGeometry(const QRect& geometry, bool force) { QRect oldGeom = m_geometry; m_geometry = geometry; if (m_geometry == oldGeom && !force) { return; } effects->addRepaint(oldGeom); effects->addRepaint(m_geometry); if (m_geometry.size() == oldGeom.size() && !force) { return; } if (m_style == EffectFrameStyled) { qreal left, top, right, bottom; m_frame.getMargins(left, top, right, bottom); // m_geometry is the inner geometry m_frame.resizeFrame(m_geometry.adjusted(-left, -top, right, bottom).size()); } free(); } const QIcon& EffectFrameImpl::icon() const { return m_icon; } void EffectFrameImpl::setIcon(const QIcon& icon) { m_icon = icon; if (isCrossFade()) { m_sceneFrame->crossFadeIcon(); } if (m_iconSize.isEmpty() && !m_icon.availableSizes().isEmpty()) { // Set a size if we don't already have one setIconSize(m_icon.availableSizes().first()); } m_sceneFrame->freeIconFrame(); } const QSize& EffectFrameImpl::iconSize() const { return m_iconSize; } void EffectFrameImpl::setIconSize(const QSize& size) { if (m_iconSize == size) { return; } m_iconSize = size; autoResize(); m_sceneFrame->freeIconFrame(); } void EffectFrameImpl::plasmaThemeChanged() { free(); } void EffectFrameImpl::render(const QRegion ®ion, double opacity, double frameOpacity) { if (m_geometry.isEmpty()) { return; // Nothing to display } m_shader = nullptr; setScreenProjectionMatrix(static_cast(effects)->scene()->screenProjectionMatrix()); effects->paintEffectFrame(this, region, opacity, frameOpacity); } void EffectFrameImpl::finalRender(QRegion region, double opacity, double frameOpacity) const { region = infiniteRegion(); // TODO: Old region doesn't seem to work with OpenGL m_sceneFrame->render(region, opacity, frameOpacity); } Qt::Alignment EffectFrameImpl::alignment() const { return m_alignment; } void EffectFrameImpl::align(QRect &geometry) { if (m_alignment & Qt::AlignLeft) geometry.moveLeft(m_point.x()); else if (m_alignment & Qt::AlignRight) geometry.moveLeft(m_point.x() - geometry.width()); else geometry.moveLeft(m_point.x() - geometry.width() / 2); if (m_alignment & Qt::AlignTop) geometry.moveTop(m_point.y()); else if (m_alignment & Qt::AlignBottom) geometry.moveTop(m_point.y() - geometry.height()); else geometry.moveTop(m_point.y() - geometry.height() / 2); } void EffectFrameImpl::setAlignment(Qt::Alignment alignment) { m_alignment = alignment; align(m_geometry); setGeometry(m_geometry); } void EffectFrameImpl::setPosition(const QPoint& point) { m_point = point; QRect geometry = m_geometry; // this is important, setGeometry need call repaint for old & new geometry align(geometry); setGeometry(geometry); } const QString& EffectFrameImpl::text() const { return m_text; } void EffectFrameImpl::setText(const QString& text) { if (m_text == text) { return; } if (isCrossFade()) { m_sceneFrame->crossFadeText(); } m_text = text; QRect oldGeom = m_geometry; autoResize(); if (oldGeom == m_geometry) { // Wasn't updated in autoResize() m_sceneFrame->freeTextFrame(); } } void EffectFrameImpl::setSelection(const QRect& selection) { if (selection == m_selectionGeometry) { return; } m_selectionGeometry = selection; if (m_selectionGeometry.size() != m_selection.frameSize().toSize()) { m_selection.resizeFrame(m_selectionGeometry.size()); } // TODO; optimize to only recreate when resizing m_sceneFrame->freeSelection(); } void EffectFrameImpl::autoResize() { if (m_static) return; // Not automatically resizing QRect geometry; // Set size if (!m_text.isEmpty()) { QFontMetrics metrics(m_font); geometry.setSize(metrics.size(0, m_text)); } if (!m_icon.isNull() && !m_iconSize.isEmpty()) { geometry.setLeft(-m_iconSize.width()); if (m_iconSize.height() > geometry.height()) geometry.setHeight(m_iconSize.height()); } align(geometry); setGeometry(geometry); } QColor EffectFrameImpl::styledTextColor() { return m_theme->color(Plasma::Theme::TextColor); } } // namespace diff --git a/placement.cpp b/placement.cpp index 1b21af2fb..f15860f52 100644 --- a/placement.cpp +++ b/placement.cpp @@ -1,977 +1,977 @@ /******************************************************************** 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" #ifndef KCMRULES #include "workspace.h" #include "x11client.h" #include "cursor.h" #include "options.h" #include "rules.h" #include "screens.h" #endif #include #include namespace KWin { #ifndef KCMRULES KWIN_SINGLETON_FACTORY(Placement) Placement::Placement(QObject*) { reinitCascading(0); } Placement::~Placement() { s_self = nullptr; } /** * Places the client \a c according to the workspace's layout policy */ void Placement::place(AbstractClient *c, const QRect &area) { Policy policy = c->rules()->checkPlacement(Default); if (policy != Default) { place(c, area, policy); return; } if (c->isUtility()) placeUtility(c, area, options->placement()); else if (c->isDialog()) placeDialog(c, area, options->placement()); else if (c->isSplash()) placeOnMainWindow(c, area); // on mainwindow, if any, otherwise centered else if (c->isOnScreenDisplay() || c->isNotification() || c->isCriticalNotification()) placeOnScreenDisplay(c, area); else if (c->isTransient() && c->hasTransientPlacementHint()) placeTransient(c); else if (c->isTransient() && c->surface()) placeDialog(c, area, options->placement()); else place(c, area, options->placement()); } void Placement::place(AbstractClient *c, const QRect &area, Policy policy, Policy nextPlacement) { if (policy == Unknown) policy = Default; if (policy == Default) policy = options->placement(); if (policy == NoPlacement) return; else if (policy == Random) placeAtRandom(c, area, nextPlacement); else if (policy == Cascade) placeCascaded(c, area, nextPlacement); else if (policy == Centered) placeCentered(c, area, nextPlacement); else if (policy == ZeroCornered) placeZeroCornered(c, area, nextPlacement); else if (policy == UnderMouse) placeUnderMouse(c, area, nextPlacement); else if (policy == OnMainWindow) placeOnMainWindow(c, area, nextPlacement); else if (policy == Maximizing) placeMaximizing(c, area, nextPlacement); else placeSmart(c, area, nextPlacement); if (options->borderSnapZone()) { // snap to titlebar / snap to window borders on inner screen edges const QRect geo(c->frameGeometry()); QPoint corner = geo.topLeft(); const QMargins frameMargins = c->frameMargins(); AbstractClient::Position titlePos = c->titlebarPosition(); const QRect fullRect = workspace()->clientArea(FullArea, c); if (!(c->maximizeMode() & MaximizeHorizontal)) { if (titlePos != AbstractClient::PositionRight && geo.right() == fullRect.right()) { corner.rx() += frameMargins.right(); } if (titlePos != AbstractClient::PositionLeft && geo.left() == fullRect.left()) { corner.rx() -= frameMargins.left(); } } if (!(c->maximizeMode() & MaximizeVertical)) { if (titlePos != AbstractClient::PositionBottom && geo.bottom() == fullRect.bottom()) { corner.ry() += frameMargins.bottom(); } if (titlePos != AbstractClient::PositionTop && geo.top() == fullRect.top()) { corner.ry() -= frameMargins.top(); } } c->move(corner); } } /** * Place the client \a c according to a simply "random" placement algorithm. */ void Placement::placeAtRandom(AbstractClient* c, const QRect& area, Policy /*next*/) { Q_ASSERT(area.isValid()); const int step = 24; static int px = step; static int py = 2 * step; int tx, ty; if (px < area.x()) { px = area.x(); } if (py < area.y()) { py = area.y(); } px += step; py += 2 * step; if (px > area.width() / 2) { px = area.x() + step; } if (py > area.height() / 2) { py = area.y() + step; } tx = px; ty = py; if (tx + c->width() > area.right()) { tx = area.right() - c->width(); if (tx < 0) tx = 0; px = area.x(); } if (ty + c->height() > area.bottom()) { ty = area.bottom() - c->height(); if (ty < 0) ty = 0; py = area.y(); } c->move(tx, ty); } // TODO: one day, there'll be C++11 ... static inline bool isIrrelevant(const AbstractClient *client, const AbstractClient *regarding, int desktop) { if (!client) return true; if (client == regarding) return true; if (!client->isShown(false)) return true; if (!client->isOnDesktop(desktop)) return true; if (!client->isOnCurrentActivity()) return true; if (client->isDesktop()) return true; return false; } /** * Place the client \a c according to a really smart placement algorithm :-) */ void Placement::placeSmart(AbstractClient* c, const QRect& area, Policy /*next*/) { Q_ASSERT(area.isValid()); /* * SmartPlacement by Cristian Tibirna (tibirna@kde.org) * adapted for kwm (16-19jan98) and for kwin (16Nov1999) using (with * permission) ideas from fvwm, authored by * Anthony Martin (amartin@engr.csulb.edu). * Xinerama supported added by Balaji Ramani (balaji@yablibli.com) * with ideas from xfce. */ - if (!c->size().isValid()) { + if (!c->frameGeometry().isValid()) { return; } const int none = 0, h_wrong = -1, w_wrong = -2; // overlap types long int overlap, min_overlap = 0; int x_optimal, y_optimal; int possible; int desktop = c->desktop() == 0 || c->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : c->desktop(); int cxl, cxr, cyt, cyb; //temp coords int xl, xr, yt, yb; //temp coords int basket; //temp holder // get the maximum allowed windows space int x = area.left(); int y = area.top(); x_optimal = x; y_optimal = y; //client gabarit int ch = c->height() - 1; int cw = c->width() - 1; bool first_pass = true; //CT lame flag. Don't like it. What else would do? //loop over possible positions do { //test if enough room in x and y directions if (y + ch > area.bottom() && ch < area.height()) { overlap = h_wrong; // this throws the algorithm to an exit } else if (x + cw > area.right()) { overlap = w_wrong; } else { overlap = none; //initialize cxl = x; cxr = x + cw; cyt = y; cyb = y + ch; for (auto l = workspace()->stackingOrder().constBegin(); l != workspace()->stackingOrder().constEnd() ; ++l) { AbstractClient *client = qobject_cast(*l); if (isIrrelevant(client, c, desktop)) { continue; } xl = client->x(); yt = client->y(); xr = xl + client->width(); yb = yt + client->height(); //if windows overlap, calc the overall overlapping if ((cxl < xr) && (cxr > xl) && (cyt < yb) && (cyb > yt)) { xl = qMax(cxl, xl); xr = qMin(cxr, xr); yt = qMax(cyt, yt); yb = qMin(cyb, yb); if (client->keepAbove()) overlap += 16 * (xr - xl) * (yb - yt); else if (client->keepBelow() && !client->isDock()) // ignore KeepBelow windows overlap += 0; // for placement (see X11Client::belongsToLayer() for Dock) else overlap += (xr - xl) * (yb - yt); } } } //CT first time we get no overlap we stop. if (overlap == none) { x_optimal = x; y_optimal = y; break; } if (first_pass) { first_pass = false; min_overlap = overlap; } //CT save the best position and the minimum overlap up to now else if (overlap >= none && overlap < min_overlap) { min_overlap = overlap; x_optimal = x; y_optimal = y; } // really need to loop? test if there's any overlap if (overlap > none) { possible = area.right(); if (possible - cw > x) possible -= cw; // compare to the position of each client on the same desk for (auto l = workspace()->stackingOrder().constBegin(); l != workspace()->stackingOrder().constEnd() ; ++l) { AbstractClient *client = qobject_cast(*l); if (isIrrelevant(client, c, desktop)) { continue; } xl = client->x(); yt = client->y(); xr = xl + client->width(); yb = yt + client->height(); // if not enough room above or under the current tested client // determine the first non-overlapped x position if ((y < yb) && (yt < ch + y)) { if ((xr > x) && (possible > xr)) possible = xr; basket = xl - cw; if ((basket > x) && (possible > basket)) possible = basket; } } x = possible; } // ... else ==> not enough x dimension (overlap was wrong on horizontal) else if (overlap == w_wrong) { x = area.left(); possible = area.bottom(); if (possible - ch > y) possible -= ch; //test the position of each window on the desk for (auto l = workspace()->stackingOrder().constBegin(); l != workspace()->stackingOrder().constEnd() ; ++l) { AbstractClient *client = qobject_cast(*l); if (isIrrelevant(client, c, desktop)) { continue; } xl = client->x(); yt = client->y(); xr = xl + client->width(); yb = yt + client->height(); // if not enough room to the left or right of the current tested client // determine the first non-overlapped y position if ((yb > y) && (possible > yb)) possible = yb; basket = yt - ch; if ((basket > y) && (possible > basket)) possible = basket; } y = possible; } } while ((overlap != none) && (overlap != h_wrong) && (y < area.bottom())); if (ch >= area.height()) { y_optimal = area.top(); } // place the window c->move(x_optimal, y_optimal); } void Placement::reinitCascading(int desktop) { // desktop == 0 - reinit all if (desktop == 0) { cci.clear(); for (uint i = 0; i < VirtualDesktopManager::self()->count(); ++i) { DesktopCascadingInfo inf; inf.pos = QPoint(-1, -1); inf.col = 0; inf.row = 0; cci.append(inf); } } else { cci[desktop - 1].pos = QPoint(-1, -1); cci[desktop - 1].col = cci[desktop - 1].row = 0; } } QPoint Workspace::cascadeOffset(const AbstractClient *c) const { QRect area = clientArea(PlacementArea, c->frameGeometry().center(), c->desktop()); return QPoint(area.width()/48, area.height()/48); } /** * Place windows in a cascading order, remembering positions for each desktop */ void Placement::placeCascaded(AbstractClient *c, const QRect &area, Policy nextPlacement) { Q_ASSERT(area.isValid()); - if (!c->size().isValid()) { + if (!c->frameGeometry().isValid()) { return; } /* cascadePlacement by Cristian Tibirna (tibirna@kde.org) (30Jan98) */ // work coords int xp, yp; //CT how do I get from the 'Client' class the size that NW squarish "handle" const QPoint delta = workspace()->cascadeOffset(c); const int dn = c->desktop() == 0 || c->isOnAllDesktops() ? (VirtualDesktopManager::self()->current() - 1) : (c->desktop() - 1); // initialize often used vars: width and height of c; we gain speed const int ch = c->height(); const int cw = c->width(); const int X = area.left(); const int Y = area.top(); const int H = area.height(); const int W = area.width(); if (nextPlacement == Unknown) nextPlacement = Smart; //initialize if needed if (cci[dn].pos.x() < 0 || cci[dn].pos.x() < X || cci[dn].pos.y() < Y) { cci[dn].pos = QPoint(X, Y); cci[dn].col = cci[dn].row = 0; } xp = cci[dn].pos.x(); yp = cci[dn].pos.y(); //here to touch in case people vote for resize on placement if ((yp + ch) > H) yp = Y; if ((xp + cw) > W) { if (!yp) { place(c, area, nextPlacement); return; } else xp = X; } //if this isn't the first window if (cci[dn].pos.x() != X && cci[dn].pos.y() != Y) { /* The following statements cause an internal compiler error with * egcs-2.91.66 on SuSE Linux 6.3. The equivalent forms compile fine. * 22-Dec-1999 CS * * if (xp != X && yp == Y) xp = delta.x() * (++(cci[dn].col)); * if (yp != Y && xp == X) yp = delta.y() * (++(cci[dn].row)); */ if (xp != X && yp == Y) { ++(cci[dn].col); xp = delta.x() * cci[dn].col; } if (yp != Y && xp == X) { ++(cci[dn].row); yp = delta.y() * cci[dn].row; } // last resort: if still doesn't fit, smart place it if (((xp + cw) > W - X) || ((yp + ch) > H - Y)) { place(c, area, nextPlacement); return; } } // place the window c->move(QPoint(xp, yp)); // new position cci[dn].pos = QPoint(xp + delta.x(), yp + delta.y()); } /** * Place windows centered, on top of all others */ void Placement::placeCentered(AbstractClient* c, const QRect& area, Policy /*next*/) { Q_ASSERT(area.isValid()); const int xp = area.left() + (area.width() - c->width()) / 2; const int yp = area.top() + (area.height() - c->height()) / 2; // place the window c->move(QPoint(xp, yp)); } /** * Place windows in the (0,0) corner, on top of all others */ void Placement::placeZeroCornered(AbstractClient* c, const QRect& area, Policy /*next*/) { Q_ASSERT(area.isValid()); // get the maximum allowed windows space and desk's origin c->move(area.topLeft()); } void Placement::placeUtility(AbstractClient *c, const QRect &area, Policy /*next*/) { // TODO kwin should try to place utility windows next to their mainwindow, // preferably at the right edge, and going down if there are more of them // if there's not enough place outside the mainwindow, it should prefer // top-right corner // use the default placement for now place(c, area, Default); } void Placement::placeOnScreenDisplay(AbstractClient *c, const QRect &area) { Q_ASSERT(area.isValid()); // place at lower area of the screen const int x = area.left() + (area.width() - c->width()) / 2; const int y = area.top() + 2 * area.height() / 3 - c->height() / 2; c->move(QPoint(x, y)); } void Placement::placeTransient(AbstractClient *c) { const auto parent = c->transientFor(); const QRect screen = Workspace::self()->clientArea(parent->isFullScreen() ? FullScreenArea : PlacementArea, parent); const QRect popupGeometry = c->transientPlacement(screen); c->setFrameGeometry(popupGeometry); // Potentially a client could set no constraint adjustments // and we'll be offscreen. // The spec implies we should place window the offscreen. However, // practically Qt doesn't set any constraint adjustments yet so we can't. // Also kwin generally doesn't let clients do what they want if (!screen.contains(c->frameGeometry())) { c->keepInArea(screen); } } void Placement::placeDialog(AbstractClient *c, const QRect &area, Policy nextPlacement) { placeOnMainWindow(c, area, nextPlacement); } void Placement::placeUnderMouse(AbstractClient *c, const QRect &area, Policy /*next*/) { Q_ASSERT(area.isValid()); QRect geom = c->frameGeometry(); geom.moveCenter(Cursors::self()->mouse()->pos()); c->move(geom.topLeft()); c->keepInArea(area); // make sure it's kept inside workarea } void Placement::placeOnMainWindow(AbstractClient *c, const QRect &area, Policy nextPlacement) { Q_ASSERT(area.isValid()); if (nextPlacement == Unknown) nextPlacement = Centered; if (nextPlacement == Maximizing) // maximize if needed placeMaximizing(c, area, NoPlacement); auto mainwindows = c->mainClients(); AbstractClient* place_on = nullptr; AbstractClient* place_on2 = nullptr; int mains_count = 0; for (auto it = mainwindows.constBegin(); it != mainwindows.constEnd(); ++it) { if (mainwindows.count() > 1 && (*it)->isSpecialWindow()) continue; // don't consider toolbars etc when placing ++mains_count; place_on2 = *it; if ((*it)->isOnCurrentDesktop()) { if (place_on == nullptr) place_on = *it; else { // two or more on current desktop -> center // That's the default at least. However, with maximizing placement // policy as the default, the dialog should be either maximized or // made as large as its maximum size and then placed centered. // So the nextPlacement argument allows chaining. In this case, nextPlacement // is Maximizing and it will call placeCentered(). place(c, area, Centered); return; } } } if (place_on == nullptr) { // 'mains_count' is used because it doesn't include ignored mainwindows if (mains_count != 1) { place(c, area, Centered); return; } place_on = place_on2; // use the only window filtered together with 'mains_count' } if (place_on->isDesktop()) { place(c, area, Centered); return; } QRect geom = c->frameGeometry(); geom.moveCenter(place_on->frameGeometry().center()); c->move(geom.topLeft()); // get area again, because the mainwindow may be on different xinerama screen const QRect placementArea = workspace()->clientArea(PlacementArea, c); c->keepInArea(placementArea); // make sure it's kept inside workarea } void Placement::placeMaximizing(AbstractClient *c, const QRect &area, Policy nextPlacement) { Q_ASSERT(area.isValid()); if (nextPlacement == Unknown) nextPlacement = Smart; if (c->isMaximizable() && c->maxSize().width() >= area.width() && c->maxSize().height() >= area.height()) { if (workspace()->clientArea(MaximizeArea, c) == area) c->maximize(MaximizeFull); else { // if the geometry doesn't match default maximize area (xinerama case?), // it's probably better to use the given area c->setFrameGeometry(area); } } else { c->resizeWithChecks(c->maxSize().boundedTo(area.size())); place(c, area, nextPlacement); } } void Placement::cascadeDesktop() { Workspace *ws = Workspace::self(); const int desktop = VirtualDesktopManager::self()->current(); reinitCascading(desktop); foreach (Toplevel *toplevel, ws->stackingOrder()) { auto client = qobject_cast(toplevel); if (!client || (!client->isOnCurrentDesktop()) || (client->isMinimized()) || (client->isOnAllDesktops()) || (!client->isMovable())) continue; const QRect placementArea = workspace()->clientArea(PlacementArea, client); placeCascaded(client, placementArea); } } void Placement::unclutterDesktop() { const auto &clients = Workspace::self()->allClientList(); for (int i = clients.size() - 1; i >= 0; i--) { auto client = clients.at(i); if ((!client->isOnCurrentDesktop()) || (client->isMinimized()) || (client->isOnAllDesktops()) || (!client->isMovable())) continue; const QRect placementArea = workspace()->clientArea(PlacementArea, client); placeSmart(client, placementArea); } } #endif Placement::Policy Placement::policyFromString(const QString& policy, bool no_special) { if (policy == QStringLiteral("NoPlacement")) return NoPlacement; else if (policy == QStringLiteral("Default") && !no_special) return Default; else if (policy == QStringLiteral("Random")) return Random; else if (policy == QStringLiteral("Cascade")) return Cascade; else if (policy == QStringLiteral("Centered")) return Centered; else if (policy == QStringLiteral("ZeroCornered")) return ZeroCornered; else if (policy == QStringLiteral("UnderMouse")) return UnderMouse; else if (policy == QStringLiteral("OnMainWindow") && !no_special) return OnMainWindow; else if (policy == QStringLiteral("Maximizing")) return Maximizing; else return Smart; } const char* Placement::policyToString(Policy policy) { const char* const policies[] = { "NoPlacement", "Default", "XXX should never see", "Random", "Smart", "Cascade", "Centered", "ZeroCornered", "UnderMouse", "OnMainWindow", "Maximizing" }; Q_ASSERT(policy < int(sizeof(policies) / sizeof(policies[ 0 ]))); return policies[ policy ]; } #ifndef KCMRULES // ******************** // Workspace // ******************** void AbstractClient::packTo(int left, int top) { workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; const int oldScreen = screen(); move(left, top); if (screen() != oldScreen) { workspace()->sendClientToScreen(this, screen()); // checks rule validity if (maximizeMode() != MaximizeRestore) checkWorkspacePosition(); } } /** * Moves active window left until in bumps into another window or workarea edge. */ void Workspace::slotWindowPackLeft() { if (active_client && active_client->isMovable()) active_client->packTo(packPositionLeft(active_client, active_client->frameGeometry().left(), true), active_client->y()); } void Workspace::slotWindowPackRight() { if (active_client && active_client->isMovable()) active_client->packTo(packPositionRight(active_client, active_client->frameGeometry().right(), true) - active_client->width() + 1, active_client->y()); } void Workspace::slotWindowPackUp() { if (active_client && active_client->isMovable()) active_client->packTo(active_client->x(), packPositionUp(active_client, active_client->frameGeometry().top(), true)); } void Workspace::slotWindowPackDown() { if (active_client && active_client->isMovable()) active_client->packTo(active_client->x(), packPositionDown(active_client, active_client->frameGeometry().bottom(), true) - active_client->height() + 1); } void Workspace::slotWindowGrowHorizontal() { if (active_client) active_client->growHorizontal(); } void AbstractClient::growHorizontal() { if (!isResizable() || isShade()) return; QRect geom = frameGeometry(); geom.setRight(workspace()->packPositionRight(this, geom.right(), true)); QSize adjsize = constrainFrameSize(geom.size(), SizeModeFixedW); if (frameGeometry().size() == adjsize && geom.size() != adjsize && resizeIncrements().width() > 1) { // take care of size increments int newright = workspace()->packPositionRight(this, geom.right() + resizeIncrements().width() - 1, true); // check that it hasn't grown outside of the area, due to size increments // TODO this may be wrong? if (workspace()->clientArea(MovementArea, QPoint((x() + newright) / 2, frameGeometry().center().y()), desktop()).right() >= newright) geom.setRight(newright); } geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedW)); geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedH)); workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; setFrameGeometry(geom); } void Workspace::slotWindowShrinkHorizontal() { if (active_client) active_client->shrinkHorizontal(); } void AbstractClient::shrinkHorizontal() { if (!isResizable() || isShade()) return; QRect geom = frameGeometry(); geom.setRight(workspace()->packPositionLeft(this, geom.right(), false)); if (geom.width() <= 1) return; geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedW)); if (geom.width() > 20) { workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; setFrameGeometry(geom); } } void Workspace::slotWindowGrowVertical() { if (active_client) active_client->growVertical(); } void AbstractClient::growVertical() { if (!isResizable() || isShade()) return; QRect geom = frameGeometry(); geom.setBottom(workspace()->packPositionDown(this, geom.bottom(), true)); QSize adjsize = constrainFrameSize(geom.size(), SizeModeFixedH); if (frameGeometry().size() == adjsize && geom.size() != adjsize && resizeIncrements().height() > 1) { // take care of size increments int newbottom = workspace()->packPositionDown(this, geom.bottom() + resizeIncrements().height() - 1, true); // check that it hasn't grown outside of the area, due to size increments if (workspace()->clientArea(MovementArea, QPoint(frameGeometry().center().x(), (y() + newbottom) / 2), desktop()).bottom() >= newbottom) geom.setBottom(newbottom); } geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedH)); workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; setFrameGeometry(geom); } void Workspace::slotWindowShrinkVertical() { if (active_client) active_client->shrinkVertical(); } void AbstractClient::shrinkVertical() { if (!isResizable() || isShade()) return; QRect geom = frameGeometry(); geom.setBottom(workspace()->packPositionUp(this, geom.bottom(), false)); if (geom.height() <= 1) return; geom.setSize(constrainFrameSize(geom.size(), SizeModeFixedH)); if (geom.height() > 20) { workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event; setFrameGeometry(geom); } } void Workspace::quickTileWindow(QuickTileMode mode) { if (!active_client) { return; } active_client->setQuickTileMode(mode, true); } int Workspace::packPositionLeft(const AbstractClient *client, int oldX, bool leftEdge) const { int newX = clientArea(MaximizeArea, client).left(); if (oldX <= newX) { // try another Xinerama screen newX = clientArea(MaximizeArea, QPoint(client->frameGeometry().left() - 1, client->frameGeometry().center().y()), client->desktop()).left(); } if (client->titlebarPosition() != AbstractClient::PositionLeft) { const int right = newX - client->frameMargins().left(); QRect frameGeometry = client->frameGeometry(); frameGeometry.moveRight(right); if (screens()->intersecting(frameGeometry) < 2) { newX = right; } } if (oldX <= newX) { return oldX; } const int desktop = client->desktop() == 0 || client->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : client->desktop(); for (auto it = m_allClients.constBegin(), end = m_allClients.constEnd(); it != end; ++it) { if (isIrrelevant(*it, client, desktop)) { continue; } const int x = leftEdge ? (*it)->frameGeometry().right() + 1 : (*it)->frameGeometry().left() - 1; if (x > newX && x < oldX && !(client->frameGeometry().top() > (*it)->frameGeometry().bottom() // they overlap in Y direction || client->frameGeometry().bottom() < (*it)->frameGeometry().top())) { newX = x; } } return newX; } int Workspace::packPositionRight(const AbstractClient *client, int oldX, bool rightEdge) const { int newX = clientArea(MaximizeArea, client).right(); if (oldX >= newX) { // try another Xinerama screen newX = clientArea(MaximizeArea, QPoint(client->frameGeometry().right() + 1, client->frameGeometry().center().y()), client->desktop()).right(); } if (client->titlebarPosition() != AbstractClient::PositionRight) { const int right = newX + client->frameMargins().right(); QRect frameGeometry = client->frameGeometry(); frameGeometry.moveRight(right); if (screens()->intersecting(frameGeometry) < 2) { newX = right; } } if (oldX >= newX) { return oldX; } const int desktop = client->desktop() == 0 || client->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : client->desktop(); for (auto it = m_allClients.constBegin(), end = m_allClients.constEnd(); it != end; ++it) { if (isIrrelevant(*it, client, desktop)) { continue; } const int x = rightEdge ? (*it)->frameGeometry().left() - 1 : (*it)->frameGeometry().right() + 1; if (x < newX && x > oldX && !(client->frameGeometry().top() > (*it)->frameGeometry().bottom() || client->frameGeometry().bottom() < (*it)->frameGeometry().top())) { newX = x; } } return newX; } int Workspace::packPositionUp(const AbstractClient *client, int oldY, bool topEdge) const { int newY = clientArea(MaximizeArea, client).top(); if (oldY <= newY) { // try another Xinerama screen newY = clientArea(MaximizeArea, QPoint(client->frameGeometry().center().x(), client->frameGeometry().top() - 1), client->desktop()).top(); } if (client->titlebarPosition() != AbstractClient::PositionTop) { const int top = newY - client->frameMargins().top(); QRect frameGeometry = client->frameGeometry(); frameGeometry.moveTop(top); if (screens()->intersecting(frameGeometry) < 2) { newY = top; } } if (oldY <= newY) { return oldY; } const int desktop = client->desktop() == 0 || client->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : client->desktop(); for (auto it = m_allClients.constBegin(), end = m_allClients.constEnd(); it != end; ++it) { if (isIrrelevant(*it, client, desktop)) { continue; } const int y = topEdge ? (*it)->frameGeometry().bottom() + 1 : (*it)->frameGeometry().top() - 1; if (y > newY && y < oldY && !(client->frameGeometry().left() > (*it)->frameGeometry().right() // they overlap in X direction || client->frameGeometry().right() < (*it)->frameGeometry().left())) { newY = y; } } return newY; } int Workspace::packPositionDown(const AbstractClient *client, int oldY, bool bottomEdge) const { int newY = clientArea(MaximizeArea, client).bottom(); if (oldY >= newY) { // try another Xinerama screen newY = clientArea(MaximizeArea, QPoint(client->frameGeometry().center().x(), client->frameGeometry().bottom() + 1), client->desktop()).bottom(); } if (client->titlebarPosition() != AbstractClient::PositionBottom) { const int bottom = newY + client->frameMargins().bottom(); QRect frameGeometry = client->frameGeometry(); frameGeometry.moveBottom(bottom); if (screens()->intersecting(frameGeometry) < 2) { newY = bottom; } } if (oldY >= newY) { return oldY; } const int desktop = client->desktop() == 0 || client->isOnAllDesktops() ? VirtualDesktopManager::self()->current() : client->desktop(); for (auto it = m_allClients.constBegin(), end = m_allClients.constEnd(); it != end; ++it) { if (isIrrelevant(*it, client, desktop)) { continue; } const int y = bottomEdge ? (*it)->frameGeometry().top() - 1 : (*it)->frameGeometry().bottom() + 1; if (y < newY && y > oldY && !(client->frameGeometry().left() > (*it)->frameGeometry().right() || client->frameGeometry().right() < (*it)->frameGeometry().left())) { newY = y; } } return newY; } #endif } // namespace diff --git a/wayland_server.cpp b/wayland_server.cpp index ca8a12b5b..deed0f32f 100644 --- a/wayland_server.cpp +++ b/wayland_server.cpp @@ -1,854 +1,868 @@ /******************************************************************** 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 "wayland_server.h" #include "x11client.h" #include "platform.h" #include "composite.h" #include "idle_inhibition.h" #include "screens.h" -#include "xdgshellclient.h" #include "workspace.h" +#include "xdgshellclient.h" // Client #include #include #include #include #include #include #include #include // Server #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include -#include +#include #include #include #include #include #include #include // KF #include // Qt #include #include #include #include #include // system #include #include #include //screenlocker #include using namespace KWaylandServer; namespace KWin { KWIN_SINGLETON_FACTORY(WaylandServer) WaylandServer::WaylandServer(QObject *parent) : QObject(parent) { qRegisterMetaType(); } WaylandServer::~WaylandServer() { destroyInputMethodConnection(); } void WaylandServer::destroyInternalConnection() { emit terminatingInternalClientConnection(); if (m_internalConnection.client) { // delete all connections hold by plugins like e.g. widget style const auto connections = KWayland::Client::ConnectionThread::connections(); for (auto c : connections) { if (c == m_internalConnection.client) { continue; } emit c->connectionDied(); } delete m_internalConnection.registry; delete m_internalConnection.compositor; delete m_internalConnection.seat; delete m_internalConnection.ddm; delete m_internalConnection.shm; dispatch(); m_internalConnection.client->deleteLater(); m_internalConnection.clientThread->quit(); m_internalConnection.clientThread->wait(); delete m_internalConnection.clientThread; m_internalConnection.client = nullptr; m_internalConnection.server->destroy(); m_internalConnection.server = nullptr; } } void WaylandServer::terminateClientConnections() { destroyInternalConnection(); destroyInputMethodConnection(); if (m_display) { const auto connections = m_display->connections(); for (auto it = connections.begin(); it != connections.end(); ++it) { (*it)->destroy(); } } } -template -void WaylandServer::createSurface(T *surface) +void WaylandServer::registerClient(AbstractClient *client) { - if (!Workspace::self()) { - // it's possible that a Surface gets created before Workspace is created + if (client->readyForPainting()) { + emit shellClientAdded(client); + } else { + connect(client, &AbstractClient::windowShown, this, &WaylandServer::shellClientShown); + } + m_clients << client; +} + +void WaylandServer::createXdgToplevelClient(XdgToplevelInterface *shellSurface) +{ + if (!workspace()) { return; } + + SurfaceInterface *surface = shellSurface->surface(); if (surface->client() == m_xwayland.client) { - // skip Xwayland clients, those are created using standard X11 way return; } if (surface->client() == m_screenLockerClientConnection) { ScreenLocker::KSldApp::self()->lockScreenShown(); } - XdgShellClient *client = new XdgShellClient(surface); - if (ServerSideDecorationInterface *deco = ServerSideDecorationInterface::get(surface->surface())) { - client->installServerSideDecoration(deco); - } + + XdgToplevelClient *client = new XdgToplevelClient(shellSurface); + registerClient(client); + auto it = std::find_if(m_plasmaShellSurfaces.begin(), m_plasmaShellSurfaces.end(), - [client] (PlasmaShellSurfaceInterface *surface) { - return client->surface() == surface->surface(); + [surface] (PlasmaShellSurfaceInterface *plasmaSurface) { + return plasmaSurface->surface() == surface; } ); if (it != m_plasmaShellSurfaces.end()) { client->installPlasmaShellSurface(*it); m_plasmaShellSurfaces.erase(it); } - if (auto menu = m_appMenuManager->appMenuForSurface(surface->surface())) { + if (auto decoration = ServerSideDecorationInterface::get(surface)) { + client->installServerDecoration(decoration); + } + if (auto menu = m_appMenuManager->appMenuForSurface(surface)) { client->installAppMenu(menu); } - if (auto palette = m_paletteManager->paletteForSurface(surface->surface())) { + if (auto palette = m_paletteManager->paletteForSurface(surface)) { client->installPalette(palette); } - m_clients << client; - if (client->readyForPainting()) { - emit shellClientAdded(client); - } else { - connect(client, &XdgShellClient::windowShown, this, &WaylandServer::shellClientShown); - } - //not directly connected as the connection is tied to client instead of this - connect(m_XdgForeign, &KWaylandServer::XdgForeignInterface::transientChanged, client, [this](KWaylandServer::SurfaceInterface *child) { + connect(m_XdgForeign, &XdgForeignInterface::transientChanged, client, [this](SurfaceInterface *child) { emit foreignTransientChanged(child); }); } +void WaylandServer::createXdgPopupClient(XdgPopupInterface *shellSurface) +{ + if (!workspace()) { + return; + } + + XdgPopupClient *client = new XdgPopupClient(shellSurface); + registerClient(client); +} + class KWinDisplay : public KWaylandServer::FilteredDisplay { public: KWinDisplay(QObject *parent) : KWaylandServer::FilteredDisplay(parent) {} static QByteArray sha256(const QString &fileName) { QFile f(fileName); if (f.open(QFile::ReadOnly)) { QCryptographicHash hash(QCryptographicHash::Sha256); if (hash.addData(&f)) { return hash.result(); } } return QByteArray(); } bool isTrustedOrigin(KWaylandServer::ClientConnection *client) const { const auto fullPathSha = sha256(client->executablePath()); const auto localSha = sha256(QLatin1String("/proc/") + QString::number(client->processId()) + QLatin1String("/exe")); const bool trusted = !localSha.isEmpty() && fullPathSha == localSha; if (!trusted) { qCWarning(KWIN_CORE) << "Could not trust" << client->executablePath() << "sha" << localSha << fullPathSha; } return trusted; } QStringList fetchRequestedInterfaces(KWaylandServer::ClientConnection *client) const { const auto serviceQuery = QStringLiteral("exist Exec and exist [X-KDE-Wayland-Interfaces] and '%1' =~ Exec").arg(client->executablePath()); const auto servicesFound = KServiceTypeTrader::self()->query(QStringLiteral("Application"), serviceQuery); if (servicesFound.isEmpty()) { qCDebug(KWIN_CORE) << "Could not find the desktop file for" << client->executablePath(); return {}; } const auto interfaces = servicesFound.first()->property("X-KDE-Wayland-Interfaces").toStringList(); qCDebug(KWIN_CORE) << "Interfaces for" << client->executablePath() << interfaces; return interfaces; } const QSet interfacesBlackList = {"org_kde_kwin_remote_access_manager", "org_kde_plasma_window_management", "org_kde_kwin_fake_input", "org_kde_kwin_keystate"}; QSet m_reported; bool allowInterface(KWaylandServer::ClientConnection *client, const QByteArray &interfaceName) override { if (client->processId() == getpid()) { return true; } if (!interfacesBlackList.contains(interfaceName)) { return true; } if (client->executablePath().isEmpty()) { qCWarning(KWIN_CORE) << "Could not identify process with pid" << client->processId(); return false; } { auto requestedInterfaces = client->property("requestedInterfaces"); if (requestedInterfaces.isNull()) { requestedInterfaces = fetchRequestedInterfaces(client); client->setProperty("requestedInterfaces", requestedInterfaces); } if (!requestedInterfaces.toStringList().contains(QString::fromUtf8(interfaceName))) { if (KWIN_CORE().isDebugEnabled()) { const QString id = client->executablePath() + QLatin1Char('|') + QString::fromUtf8(interfaceName); if (!m_reported.contains({id})) { m_reported.insert(id); qCDebug(KWIN_CORE) << "Interface" << interfaceName << "not in X-KDE-Wayland-Interfaces of" << client->executablePath(); } } return false; } } { auto trustedOrigin = client->property("isPrivileged"); if (trustedOrigin.isNull()) { trustedOrigin = isTrustedOrigin(client); client->setProperty("isPrivileged", trustedOrigin); } if (!trustedOrigin.toBool()) { return false; } } qCDebug(KWIN_CORE) << "authorized" << client->executablePath() << interfaceName; return true; } }; bool WaylandServer::init(const QByteArray &socketName, InitializationFlags flags) { m_initFlags = flags; m_display = new KWinDisplay(this); if (!socketName.isNull() && !socketName.isEmpty()) { m_display->setSocketName(QString::fromUtf8(socketName)); } else { m_display->setAutomaticSocketNaming(true); } m_display->start(); if (!m_display->isRunning()) { return false; } m_compositor = m_display->createCompositor(m_display); m_compositor->create(); connect(m_compositor, &CompositorInterface::surfaceCreated, this, [this] (SurfaceInterface *surface) { // check whether we have a Toplevel with the Surface's id Workspace *ws = Workspace::self(); if (!ws) { // it's possible that a Surface gets created before Workspace is created return; } if (surface->client() != xWaylandConnection()) { // setting surface is only relevat for Xwayland clients return; } auto check = [surface] (const Toplevel *t) { return t->surfaceId() == surface->id(); }; if (Toplevel *t = ws->findToplevel(check)) { t->setSurface(surface); } } ); m_tabletManager = m_display->createTabletManagerInterface(m_display); m_keyboardShortcutsInhibitManager = m_display->createKeyboardShortcutsInhibitManagerV1(m_display); - m_xdgShell = m_display->createXdgShell(XdgShellInterfaceVersion::Stable, m_display); - m_xdgShell->create(); - connect(m_xdgShell, &XdgShellInterface::surfaceCreated, this, &WaylandServer::createSurface); - connect(m_xdgShell, &XdgShellInterface::xdgPopupCreated, this, &WaylandServer::createSurface); - - m_xdgDecorationManager = m_display->createXdgDecorationManager(m_xdgShell, m_display); - m_xdgDecorationManager->create(); - connect(m_xdgDecorationManager, &XdgDecorationManagerInterface::xdgDecorationInterfaceCreated, this, [this] (XdgDecorationInterface *deco) { - if (XdgShellClient *client = findXdgShellClient(deco->surface()->surface())) { - client->installXdgDecoration(deco); + + m_xdgShell = m_display->createXdgShell(m_display); + connect(m_xdgShell, &XdgShellInterface::toplevelCreated, this, &WaylandServer::createXdgToplevelClient); + connect(m_xdgShell, &XdgShellInterface::popupCreated, this, &WaylandServer::createXdgPopupClient); + + m_xdgDecorationManagerV1 = m_display->createXdgDecorationManagerV1(m_display); + connect(m_xdgDecorationManagerV1, &XdgDecorationManagerV1Interface::decorationCreated, this, + [this](XdgToplevelDecorationV1Interface *decoration) { + if (XdgToplevelClient *toplevel = findXdgToplevelClient(decoration->toplevel()->surface())) { + toplevel->installXdgDecoration(decoration); + } } - }); + ); m_display->createShm(); m_seat = m_display->createSeat(m_display); m_seat->create(); m_display->createPointerGestures(PointerGesturesInterfaceVersion::UnstableV1, m_display)->create(); m_display->createPointerConstraints(PointerConstraintsInterfaceVersion::UnstableV1, m_display)->create(); m_dataDeviceManager = m_display->createDataDeviceManager(m_display); m_dataDeviceManager->create(); m_display->createDataControlDeviceManagerV1(m_display); m_idle = m_display->createIdle(m_display); m_idle->create(); auto idleInhibition = new IdleInhibition(m_idle); connect(this, &WaylandServer::shellClientAdded, idleInhibition, &IdleInhibition::registerClient); m_display->createIdleInhibitManager(IdleInhibitManagerInterfaceVersion::UnstableV1, m_display)->create(); m_plasmaShell = m_display->createPlasmaShell(m_display); m_plasmaShell->create(); connect(m_plasmaShell, &PlasmaShellInterface::surfaceCreated, [this] (PlasmaShellSurfaceInterface *surface) { - if (XdgShellClient *client = findXdgShellClient(surface->surface())) { + if (XdgToplevelClient *client = findXdgToplevelClient(surface->surface())) { client->installPlasmaShellSurface(surface); - } else { - m_plasmaShellSurfaces << surface; - connect(surface, &QObject::destroyed, this, - [this, surface] { - m_plasmaShellSurfaces.removeOne(surface); - } - ); + return; } + m_plasmaShellSurfaces.append(surface); + connect(surface, &QObject::destroyed, this, [this, surface] { + m_plasmaShellSurfaces.removeOne(surface); + }); } ); m_appMenuManager = m_display->createAppMenuManagerInterface(m_display); m_appMenuManager->create(); connect(m_appMenuManager, &AppMenuManagerInterface::appMenuCreated, [this] (AppMenuInterface *appMenu) { - if (XdgShellClient *client = findXdgShellClient(appMenu->surface())) { + if (XdgToplevelClient *client = findXdgToplevelClient(appMenu->surface())) { client->installAppMenu(appMenu); } } ); m_paletteManager = m_display->createServerSideDecorationPaletteManager(m_display); m_paletteManager->create(); connect(m_paletteManager, &ServerSideDecorationPaletteManagerInterface::paletteCreated, [this] (ServerSideDecorationPaletteInterface *palette) { - if (XdgShellClient *client = findXdgShellClient(palette->surface())) { + if (XdgToplevelClient *client = findXdgToplevelClient(palette->surface())) { client->installPalette(palette); } } ); m_windowManagement = m_display->createPlasmaWindowManagement(m_display); m_windowManagement->create(); m_windowManagement->setShowingDesktopState(PlasmaWindowManagementInterface::ShowingDesktopState::Disabled); connect(m_windowManagement, &PlasmaWindowManagementInterface::requestChangeShowingDesktop, this, [] (PlasmaWindowManagementInterface::ShowingDesktopState state) { if (!workspace()) { return; } bool set = false; switch (state) { case PlasmaWindowManagementInterface::ShowingDesktopState::Disabled: set = false; break; case PlasmaWindowManagementInterface::ShowingDesktopState::Enabled: set = true; break; default: Q_UNREACHABLE(); break; } if (set == workspace()->showingDesktop()) { return; } workspace()->setShowingDesktop(set); } ); m_virtualDesktopManagement = m_display->createPlasmaVirtualDesktopManagement(m_display); m_virtualDesktopManagement->create(); m_windowManagement->setPlasmaVirtualDesktopManagementInterface(m_virtualDesktopManagement); auto shadowManager = m_display->createShadowManager(m_display); shadowManager->create(); m_display->createDpmsManager(m_display)->create(); m_decorationManager = m_display->createServerSideDecorationManager(m_display); connect(m_decorationManager, &ServerSideDecorationManagerInterface::decorationCreated, this, - [this] (ServerSideDecorationInterface *deco) { - if (XdgShellClient *c = findXdgShellClient(deco->surface())) { - c->installServerSideDecoration(deco); + [this] (ServerSideDecorationInterface *decoration) { + if (XdgToplevelClient *client = findXdgToplevelClient(decoration->surface())) { + client->installServerDecoration(decoration); } - connect(deco, &ServerSideDecorationInterface::modeRequested, this, - [deco] (ServerSideDecorationManagerInterface::Mode mode) { + connect(decoration, &ServerSideDecorationInterface::modeRequested, this, + [decoration] (ServerSideDecorationManagerInterface::Mode mode) { // always acknowledge the requested mode - deco->setMode(mode); + decoration->setMode(mode); } ); } ); m_decorationManager->create(); m_outputManagement = m_display->createOutputManagement(m_display); connect(m_outputManagement, &OutputManagementInterface::configurationChangeRequested, this, [](KWaylandServer::OutputConfigurationInterface *config) { kwinApp()->platform()->requestOutputsChange(config); }); m_outputManagement->create(); m_xdgOutputManager = m_display->createXdgOutputManager(m_display); m_xdgOutputManager->create(); m_display->createSubCompositor(m_display)->create(); m_XdgForeign = m_display->createXdgForeignInterface(m_display); m_XdgForeign->create(); m_keyState = m_display->createKeyStateInterface(m_display); m_keyState->create(); return true; } KWaylandServer::LinuxDmabufUnstableV1Interface *WaylandServer::linuxDmabuf() { if (!m_linuxDmabuf) { m_linuxDmabuf = m_display->createLinuxDmabufInterface(m_display); m_linuxDmabuf->create(); } return m_linuxDmabuf; } SurfaceInterface *WaylandServer::findForeignTransientForSurface(SurfaceInterface *surface) { return m_XdgForeign->transientFor(surface); } -void WaylandServer::shellClientShown(Toplevel *t) +void WaylandServer::shellClientShown(Toplevel *toplevel) { - XdgShellClient *c = dynamic_cast(t); - if (!c) { - qCWarning(KWIN_CORE) << "Failed to cast a Toplevel which is supposed to be a XdgShellClient to XdgShellClient"; + AbstractClient *client = qobject_cast(toplevel); + if (!client) { + qCWarning(KWIN_CORE) << "Failed to cast a Toplevel which is supposed to be an AbstractClient to AbstractClient"; return; } - disconnect(c, &XdgShellClient::windowShown, this, &WaylandServer::shellClientShown); - emit shellClientAdded(c); + disconnect(client, &AbstractClient::windowShown, this, &WaylandServer::shellClientShown); + emit shellClientAdded(client); } void WaylandServer::initWorkspace() { VirtualDesktopManager::self()->setVirtualDesktopManagement(m_virtualDesktopManagement); if (m_windowManagement) { connect(workspace(), &Workspace::showingDesktopChanged, this, [this] (bool set) { using namespace KWaylandServer; m_windowManagement->setShowingDesktopState(set ? PlasmaWindowManagementInterface::ShowingDesktopState::Enabled : PlasmaWindowManagementInterface::ShowingDesktopState::Disabled ); } ); connect(workspace(), &Workspace::workspaceInitialized, this, [this] { auto f = [this] () { QVector ids; for (Toplevel *toplevel : workspace()->stackingOrder()) { auto *client = qobject_cast(toplevel); if (client && client->windowManagementInterface()) { ids << client->windowManagementInterface()->internalId(); } } m_windowManagement->setStackingOrder(ids); }; f(); connect(workspace(), &Workspace::stackingOrderChanged, this, f); }); } if (hasScreenLockerIntegration()) { if (m_internalConnection.interfacesAnnounced) { initScreenLocker(); } else { connect(m_internalConnection.registry, &KWayland::Client::Registry::interfacesAnnounced, this, &WaylandServer::initScreenLocker); } } else { emit initialized(); } } void WaylandServer::initScreenLocker() { auto *screenLockerApp = ScreenLocker::KSldApp::self(); ScreenLocker::KSldApp::self()->setGreeterEnvironment(kwinApp()->processStartupEnvironment()); ScreenLocker::KSldApp::self()->initialize(); connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::aboutToLock, this, [this, screenLockerApp] () { if (m_screenLockerClientConnection) { // Already sent data to KScreenLocker. return; } int clientFd = createScreenLockerConnection(); if (clientFd < 0) { return; } ScreenLocker::KSldApp::self()->setWaylandFd(clientFd); for (auto *seat : m_display->seats()) { connect(seat, &KWaylandServer::SeatInterface::timestampChanged, screenLockerApp, &ScreenLocker::KSldApp::userActivity); } } ); connect(ScreenLocker::KSldApp::self(), &ScreenLocker::KSldApp::unlocked, this, [this, screenLockerApp] () { if (m_screenLockerClientConnection) { m_screenLockerClientConnection->destroy(); delete m_screenLockerClientConnection; m_screenLockerClientConnection = nullptr; } for (auto *seat : m_display->seats()) { disconnect(seat, &KWaylandServer::SeatInterface::timestampChanged, screenLockerApp, &ScreenLocker::KSldApp::userActivity); } ScreenLocker::KSldApp::self()->setWaylandFd(-1); } ); if (m_initFlags.testFlag(InitializationFlag::LockScreen)) { ScreenLocker::KSldApp::self()->lock(ScreenLocker::EstablishLock::Immediate); } emit initialized(); } WaylandServer::SocketPairConnection WaylandServer::createConnection() { SocketPairConnection ret; int sx[2]; if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sx) < 0) { qCWarning(KWIN_CORE) << "Could not create socket"; return ret; } ret.connection = m_display->createClient(sx[0]); ret.fd = sx[1]; return ret; } int WaylandServer::createScreenLockerConnection() { const auto socket = createConnection(); if (!socket.connection) { return -1; } m_screenLockerClientConnection = socket.connection; connect(m_screenLockerClientConnection, &KWaylandServer::ClientConnection::disconnected, this, [this] { m_screenLockerClientConnection = nullptr; }); return socket.fd; } int WaylandServer::createXWaylandConnection() { const auto socket = createConnection(); if (!socket.connection) { return -1; } m_xwayland.client = socket.connection; m_xwayland.destroyConnection = connect(m_xwayland.client, &KWaylandServer::ClientConnection::disconnected, this, [] { qFatal("Xwayland Connection died"); } ); return socket.fd; } void WaylandServer::destroyXWaylandConnection() { if (!m_xwayland.client) { return; } disconnect(m_xwayland.destroyConnection); m_xwayland.client->destroy(); m_xwayland.client = nullptr; } int WaylandServer::createInputMethodConnection() { const auto socket = createConnection(); if (!socket.connection) { return -1; } m_inputMethodServerConnection = socket.connection; return socket.fd; } void WaylandServer::destroyInputMethodConnection() { if (!m_inputMethodServerConnection) { return; } m_inputMethodServerConnection->destroy(); m_inputMethodServerConnection = nullptr; } void WaylandServer::createInternalConnection() { const auto socket = createConnection(); if (!socket.connection) { return; } m_internalConnection.server = socket.connection; using namespace KWayland::Client; m_internalConnection.client = new ConnectionThread(); m_internalConnection.client->setSocketFd(socket.fd); m_internalConnection.clientThread = new QThread; m_internalConnection.client->moveToThread(m_internalConnection.clientThread); m_internalConnection.clientThread->start(); connect(m_internalConnection.client, &ConnectionThread::connected, this, [this] { Registry *registry = new Registry(this); EventQueue *eventQueue = new EventQueue(registry); eventQueue->setup(m_internalConnection.client); registry->setEventQueue(eventQueue); registry->create(m_internalConnection.client); m_internalConnection.registry = registry; connect(registry, &Registry::shmAnnounced, this, [this] (quint32 name, quint32 version) { m_internalConnection.shm = m_internalConnection.registry->createShmPool(name, version, this); } ); connect(registry, &Registry::interfacesAnnounced, this, [this, registry] { m_internalConnection.interfacesAnnounced = true; const auto compInterface = registry->interface(Registry::Interface::Compositor); if (compInterface.name != 0) { m_internalConnection.compositor = registry->createCompositor(compInterface.name, compInterface.version, this); } const auto seatInterface = registry->interface(Registry::Interface::Seat); if (seatInterface.name != 0) { m_internalConnection.seat = registry->createSeat(seatInterface.name, seatInterface.version, this); } const auto ddmInterface = registry->interface(Registry::Interface::DataDeviceManager); if (ddmInterface.name != 0) { m_internalConnection.ddm = registry->createDataDeviceManager(ddmInterface.name, ddmInterface.version, this); } } ); registry->setup(); } ); m_internalConnection.client->initConnection(); } void WaylandServer::removeClient(AbstractClient *c) { m_clients.removeAll(c); emit shellClientRemoved(c); } void WaylandServer::dispatch() { if (!m_display) { return; } if (m_internalConnection.server) { m_internalConnection.server->flush(); } m_display->dispatchEvents(0); } static AbstractClient *findClientInList(const QList &clients, quint32 id) { auto it = std::find_if(clients.begin(), clients.end(), [id] (AbstractClient *c) { return c->windowId() == id; } ); if (it == clients.end()) { return nullptr; } return *it; } static AbstractClient *findClientInList(const QList &clients, KWaylandServer::SurfaceInterface *surface) { auto it = std::find_if(clients.begin(), clients.end(), [surface] (AbstractClient *c) { return c->surface() == surface; } ); if (it == clients.end()) { return nullptr; } return *it; } AbstractClient *WaylandServer::findClient(quint32 id) const { if (id == 0) { return nullptr; } if (AbstractClient *c = findClientInList(m_clients, id)) { return c; } return nullptr; } AbstractClient *WaylandServer::findClient(SurfaceInterface *surface) const { if (!surface) { return nullptr; } if (AbstractClient *c = findClientInList(m_clients, surface)) { return c; } return nullptr; } -XdgShellClient *WaylandServer::findXdgShellClient(SurfaceInterface *surface) const +XdgToplevelClient *WaylandServer::findXdgToplevelClient(SurfaceInterface *surface) const { - return qobject_cast(findClient(surface)); + return qobject_cast(findClient(surface)); } quint32 WaylandServer::createWindowId(SurfaceInterface *surface) { auto it = m_clientIds.constFind(surface->client()); quint16 clientId = 0; if (it != m_clientIds.constEnd()) { clientId = it.value(); } else { clientId = createClientId(surface->client()); } Q_ASSERT(clientId != 0); quint32 id = clientId; // TODO: this does not prevent that two surfaces of same client get same id id = (id << 16) | (surface->id() & 0xFFFF); if (findClient(id)) { qCWarning(KWIN_CORE) << "Invalid client windowId generated:" << id; return 0; } return id; } quint16 WaylandServer::createClientId(ClientConnection *c) { const QSet ids(m_clientIds.constBegin(), m_clientIds.constEnd()); quint16 id = 1; if (!ids.isEmpty()) { for (quint16 i = ids.count() + 1; i >= 1 ; i--) { if (!ids.contains(i)) { id = i; break; } } } Q_ASSERT(!ids.contains(id)); m_clientIds.insert(c, id); connect(c, &ClientConnection::disconnected, this, [this] (ClientConnection *c) { m_clientIds.remove(c); } ); return id; } bool WaylandServer::isScreenLocked() const { if (!hasScreenLockerIntegration()) { return false; } return ScreenLocker::KSldApp::self()->lockState() == ScreenLocker::KSldApp::Locked || ScreenLocker::KSldApp::self()->lockState() == ScreenLocker::KSldApp::AcquiringLock; } bool WaylandServer::hasScreenLockerIntegration() const { return !m_initFlags.testFlag(InitializationFlag::NoLockScreenIntegration); } bool WaylandServer::hasGlobalShortcutSupport() const { return !m_initFlags.testFlag(InitializationFlag::NoGlobalShortcuts); } void WaylandServer::simulateUserActivity() { if (m_idle) { m_idle->simulateUserActivity(); } } void WaylandServer::updateKeyState(KWin::Xkb::LEDs leds) { if (!m_keyState) return; m_keyState->setState(KeyStateInterface::Key::CapsLock, leds & KWin::Xkb::LED::CapsLock ? KeyStateInterface::State::Locked : KeyStateInterface::State::Unlocked); m_keyState->setState(KeyStateInterface::Key::NumLock, leds & KWin::Xkb::LED::NumLock ? KeyStateInterface::State::Locked : KeyStateInterface::State::Unlocked); m_keyState->setState(KeyStateInterface::Key::ScrollLock, leds & KWin::Xkb::LED::ScrollLock ? KeyStateInterface::State::Locked : KeyStateInterface::State::Unlocked); } bool WaylandServer::isKeyboardShortcutsInhibited() const { auto surface = seat()->focusedKeyboardSurface(); if (surface) { auto inhibitor = keyboardShortcutsInhibitManager()->findInhibitor(surface, seat()); return inhibitor && inhibitor->isActive(); } return false; } } diff --git a/wayland_server.h b/wayland_server.h index 452502529..0a6394177 100644 --- a/wayland_server.h +++ b/wayland_server.h @@ -1,321 +1,324 @@ /******************************************************************** 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_SERVER_H #define KWIN_WAYLAND_SERVER_H #include #include "keyboard_input.h" #include class QThread; class QProcess; class QWindow; namespace KWayland { namespace Client { class ConnectionThread; class Registry; class Compositor; class Seat; class DataDeviceManager; class ShmPool; class Surface; } } namespace KWaylandServer { class AppMenuManagerInterface; class ClientConnection; class CompositorInterface; class Display; class DataDeviceInterface; class IdleInterface; class SeatInterface; class DataDeviceManagerInterface; class ServerSideDecorationManagerInterface; class ServerSideDecorationPaletteManagerInterface; class SurfaceInterface; class OutputInterface; class PlasmaShellInterface; class PlasmaShellSurfaceInterface; class PlasmaVirtualDesktopManagementInterface; class PlasmaWindowManagementInterface; class OutputManagementInterface; class OutputConfigurationInterface; -class XdgDecorationManagerInterface; -class XdgShellInterface; class XdgForeignInterface; class XdgOutputManagerInterface; class KeyStateInterface; class LinuxDmabufUnstableV1Interface; class LinuxDmabufUnstableV1Buffer; class TabletManagerInterface; class KeyboardShortcutsInhibitManagerV1Interface; +class XdgDecorationManagerV1Interface; +class XdgShellInterface; +class XdgToplevelInterface; +class XdgPopupInterface; } namespace KWin { -class XdgShellClient; class AbstractClient; class Toplevel; +class XdgToplevelClient; class KWIN_EXPORT WaylandServer : public QObject { Q_OBJECT public: enum class InitializationFlag { NoOptions = 0x0, LockScreen = 0x1, NoLockScreenIntegration = 0x2, NoGlobalShortcuts = 0x4 }; Q_DECLARE_FLAGS(InitializationFlags, InitializationFlag) ~WaylandServer() override; bool init(const QByteArray &socketName = QByteArray(), InitializationFlags flags = InitializationFlag::NoOptions); void terminateClientConnections(); KWaylandServer::Display *display() const { return m_display; } KWaylandServer::CompositorInterface *compositor() const { return m_compositor; } KWaylandServer::SeatInterface *seat() const { return m_seat; } KWaylandServer::TabletManagerInterface *tabletManager() const { return m_tabletManager; } KWaylandServer::DataDeviceManagerInterface *dataDeviceManager() const { return m_dataDeviceManager; } KWaylandServer::PlasmaVirtualDesktopManagementInterface *virtualDesktopManagement() const { return m_virtualDesktopManagement; } KWaylandServer::PlasmaWindowManagementInterface *windowManagement() const { return m_windowManagement; } KWaylandServer::ServerSideDecorationManagerInterface *decorationManager() const { return m_decorationManager; } KWaylandServer::XdgOutputManagerInterface *xdgOutputManager() const { return m_xdgOutputManager; } KWaylandServer::KeyboardShortcutsInhibitManagerV1Interface *keyboardShortcutsInhibitManager() const { return m_keyboardShortcutsInhibitManager; } bool isKeyboardShortcutsInhibited() const; KWaylandServer::LinuxDmabufUnstableV1Interface *linuxDmabuf(); QList clients() const { return m_clients; } void removeClient(AbstractClient *c); AbstractClient *findClient(quint32 id) const; AbstractClient *findClient(KWaylandServer::SurfaceInterface *surface) const; - XdgShellClient *findXdgShellClient(KWaylandServer::SurfaceInterface *surface) const; + XdgToplevelClient *findXdgToplevelClient(KWaylandServer::SurfaceInterface *surface) const; /** * @returns a transient parent of a surface imported with the foreign protocol, if any */ KWaylandServer::SurfaceInterface *findForeignTransientForSurface(KWaylandServer::SurfaceInterface *surface); /** * @returns file descriptor for Xwayland to connect to. */ int createXWaylandConnection(); void destroyXWaylandConnection(); /** * @returns file descriptor to the input method server's socket. */ int createInputMethodConnection(); void destroyInputMethodConnection(); /** * @returns true if screen is locked. */ bool isScreenLocked() const; /** * @returns whether integration with KScreenLocker is available. */ bool hasScreenLockerIntegration() const; /** * @returns whether any kind of global shortcuts are supported. */ bool hasGlobalShortcutSupport() const; void createInternalConnection(); void initWorkspace(); KWaylandServer::ClientConnection *xWaylandConnection() const { return m_xwayland.client; } KWaylandServer::ClientConnection *inputMethodConnection() const { return m_inputMethodServerConnection; } KWaylandServer::ClientConnection *internalConnection() const { return m_internalConnection.server; } KWaylandServer::ClientConnection *screenLockerClientConnection() const { return m_screenLockerClientConnection; } KWayland::Client::Compositor *internalCompositor() { return m_internalConnection.compositor; } KWayland::Client::Seat *internalSeat() { return m_internalConnection.seat; } KWayland::Client::DataDeviceManager *internalDataDeviceManager() { return m_internalConnection.ddm; } KWayland::Client::ShmPool *internalShmPool() { return m_internalConnection.shm; } KWayland::Client::ConnectionThread *internalClientConection() { return m_internalConnection.client; } KWayland::Client::Registry *internalClientRegistry() { return m_internalConnection.registry; } void dispatch(); quint32 createWindowId(KWaylandServer::SurfaceInterface *surface); /** * Struct containing information for a created Wayland connection through a * socketpair. */ struct SocketPairConnection { /** * ServerSide Connection */ KWaylandServer::ClientConnection *connection = nullptr; /** * client-side file descriptor for the socket */ int fd = -1; }; /** * Creates a Wayland connection using a socket pair. */ SocketPairConnection createConnection(); void simulateUserActivity(); void updateKeyState(KWin::Xkb::LEDs leds); QSet linuxDmabufBuffers() const { return m_linuxDmabufBuffers; } void addLinuxDmabufBuffer(KWaylandServer::LinuxDmabufUnstableV1Buffer *buffer) { m_linuxDmabufBuffers << buffer; } void removeLinuxDmabufBuffer(KWaylandServer::LinuxDmabufUnstableV1Buffer *buffer) { m_linuxDmabufBuffers.remove(buffer); } Q_SIGNALS: void shellClientAdded(KWin::AbstractClient *); void shellClientRemoved(KWin::AbstractClient *); void terminatingInternalClientConnection(); void initialized(); void foreignTransientChanged(KWaylandServer::SurfaceInterface *child); private: int createScreenLockerConnection(); void shellClientShown(Toplevel *t); quint16 createClientId(KWaylandServer::ClientConnection *c); void destroyInternalConnection(); - template - void createSurface(T *surface); void initScreenLocker(); + void createXdgToplevelClient(KWaylandServer::XdgToplevelInterface *shellSurface); + void createXdgPopupClient(KWaylandServer::XdgPopupInterface *shellSurface); + void registerClient(AbstractClient *client); KWaylandServer::Display *m_display = nullptr; KWaylandServer::CompositorInterface *m_compositor = nullptr; KWaylandServer::SeatInterface *m_seat = nullptr; KWaylandServer::TabletManagerInterface *m_tabletManager = nullptr; KWaylandServer::DataDeviceManagerInterface *m_dataDeviceManager = nullptr; KWaylandServer::XdgShellInterface *m_xdgShell = nullptr; KWaylandServer::PlasmaShellInterface *m_plasmaShell = nullptr; KWaylandServer::PlasmaWindowManagementInterface *m_windowManagement = nullptr; KWaylandServer::PlasmaVirtualDesktopManagementInterface *m_virtualDesktopManagement = nullptr; KWaylandServer::ServerSideDecorationManagerInterface *m_decorationManager = nullptr; KWaylandServer::OutputManagementInterface *m_outputManagement = nullptr; KWaylandServer::AppMenuManagerInterface *m_appMenuManager = nullptr; KWaylandServer::ServerSideDecorationPaletteManagerInterface *m_paletteManager = nullptr; KWaylandServer::IdleInterface *m_idle = nullptr; KWaylandServer::XdgOutputManagerInterface *m_xdgOutputManager = nullptr; - KWaylandServer::XdgDecorationManagerInterface *m_xdgDecorationManager = nullptr; + KWaylandServer::XdgDecorationManagerV1Interface *m_xdgDecorationManagerV1 = nullptr; KWaylandServer::LinuxDmabufUnstableV1Interface *m_linuxDmabuf = nullptr; KWaylandServer::KeyboardShortcutsInhibitManagerV1Interface *m_keyboardShortcutsInhibitManager = nullptr; QSet m_linuxDmabufBuffers; struct { KWaylandServer::ClientConnection *client = nullptr; QMetaObject::Connection destroyConnection; } m_xwayland; KWaylandServer::ClientConnection *m_inputMethodServerConnection = nullptr; KWaylandServer::ClientConnection *m_screenLockerClientConnection = nullptr; struct { KWaylandServer::ClientConnection *server = nullptr; KWayland::Client::ConnectionThread *client = nullptr; QThread *clientThread = nullptr; KWayland::Client::Registry *registry = nullptr; KWayland::Client::Compositor *compositor = nullptr; KWayland::Client::Seat *seat = nullptr; KWayland::Client::DataDeviceManager *ddm = nullptr; KWayland::Client::ShmPool *shm = nullptr; bool interfacesAnnounced = false; } m_internalConnection; KWaylandServer::XdgForeignInterface *m_XdgForeign = nullptr; KWaylandServer::KeyStateInterface *m_keyState = nullptr; QList m_clients; QHash m_clientIds; InitializationFlags m_initFlags; QVector m_plasmaShellSurfaces; KWIN_SINGLETON(WaylandServer) }; inline WaylandServer *waylandServer() { return WaylandServer::self(); } } // namespace KWin #endif diff --git a/waylandclient.cpp b/waylandclient.cpp new file mode 100644 index 000000000..f91971838 --- /dev/null +++ b/waylandclient.cpp @@ -0,0 +1,287 @@ +/* + * Copyright (C) 2015 Martin Flöser + * Copyright (C) 2018 David Edmundson + * Copyright (C) 2020 Vlad Zahorodnii + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "waylandclient.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include + +#include + +#include + +#include +#include + +using namespace KWaylandServer; + +namespace KWin +{ + +WaylandClient::WaylandClient(SurfaceInterface *surface) +{ + // Note that we cannot setup compositing here because we may need to call visibleRect(), + // which in its turn will call bufferGeometry(), which is a pure virtual method. + + setSurface(surface); + + m_windowId = waylandServer()->createWindowId(surface); + + connect(this, &WaylandClient::frameGeometryChanged, + this, &WaylandClient::updateClientOutputs); + connect(this, &WaylandClient::frameGeometryChanged, + this, &WaylandClient::updateClientArea); + connect(this, &WaylandClient::desktopFileNameChanged, + this, &WaylandClient::updateIcon); + connect(screens(), &Screens::changed, this, + &WaylandClient::updateClientOutputs); + + updateResourceName(); + updateIcon(); +} + +QString WaylandClient::captionNormal() const +{ + return m_captionNormal; +} + +QString WaylandClient::captionSuffix() const +{ + return m_captionSuffix; +} + +QStringList WaylandClient::activities() const +{ + return QStringList(); +} + +void WaylandClient::setOnAllActivities(bool set) +{ + Q_UNUSED(set) +} + +void WaylandClient::blockActivityUpdates(bool b) +{ + Q_UNUSED(b) +} + +QPoint WaylandClient::clientContentPos() const +{ + return -clientPos(); +} + +QRect WaylandClient::transparentRect() const +{ + return QRect(); +} + +quint32 WaylandClient::windowId() const +{ + return m_windowId; +} + +pid_t WaylandClient::pid() const +{ + return surface()->client()->processId(); +} + +bool WaylandClient::isLockScreen() const +{ + return surface()->client() == waylandServer()->screenLockerClientConnection(); +} + +bool WaylandClient::isInputMethod() const +{ + return surface()->client() == waylandServer()->inputMethodConnection(); +} + +bool WaylandClient::isLocalhost() const +{ + return true; +} + +double WaylandClient::opacity() const +{ + return m_opacity; +} + +void WaylandClient::setOpacity(double opacity) +{ + const qreal newOpacity = qBound(0.0, opacity, 1.0); + if (newOpacity == m_opacity) { + return; + } + const qreal oldOpacity = m_opacity; + m_opacity = newOpacity; + addRepaintFull(); + emit opacityChanged(this, oldOpacity); +} + +AbstractClient *WaylandClient::findModal(bool allow_itself) +{ + Q_UNUSED(allow_itself) + return nullptr; +} + +void WaylandClient::resizeWithChecks(const QSize &size, ForceGeometry_t force) +{ + const QRect area = workspace()->clientArea(WorkArea, this); + + int width = size.width(); + int height = size.height(); + + // don't allow growing larger than workarea + if (width > area.width()) { + width = area.width(); + } + if (height > area.height()) { + height = area.height(); + } + setFrameGeometry(QRect(x(), y(), width, height), force); +} + +void WaylandClient::killWindow() +{ + if (!surface()) { + return; + } + auto c = surface()->client(); + if (c->processId() == getpid() || c->processId() == 0) { + c->destroy(); + return; + } + ::kill(c->processId(), SIGTERM); + // give it time to terminate and only if terminate fails, try destroy Wayland connection + QTimer::singleShot(5000, c, &ClientConnection::destroy); +} + +QByteArray WaylandClient::windowRole() const +{ + return QByteArray(); +} + +bool WaylandClient::belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const +{ + if (checks.testFlag(SameApplicationCheck::AllowCrossProcesses)) { + if (other->desktopFileName() == desktopFileName()) { + return true; + } + } + if (auto s = other->surface()) { + return s->client() == surface()->client(); + } + return false; +} + +bool WaylandClient::belongsToDesktop() const +{ + const auto clients = waylandServer()->clients(); + + return std::any_of(clients.constBegin(), clients.constEnd(), + [this](const AbstractClient *client) { + if (belongsToSameApplication(client, SameApplicationChecks())) { + return client->isDesktop(); + } + return false; + } + ); +} + +void WaylandClient::updateClientArea() +{ + if (hasStrut()) { + workspace()->updateClientArea(); + } +} + +void WaylandClient::updateClientOutputs() +{ + QVector clientOutputs; + const auto outputs = waylandServer()->display()->outputs(); + for (const auto output : outputs) { + if (output->isEnabled()) { + const QRect outputGeometry(output->globalPosition(), output->pixelSize() / output->scale()); + if (frameGeometry().intersects(outputGeometry)) { + clientOutputs << output; + } + } + } + surface()->setOutputs(clientOutputs); +} + +void WaylandClient::updateIcon() +{ + const QString waylandIconName = QStringLiteral("wayland"); + const QString dfIconName = iconFromDesktopFile(); + const QString iconName = dfIconName.isEmpty() ? waylandIconName : dfIconName; + if (iconName == icon().name()) { + return; + } + setIcon(QIcon::fromTheme(iconName)); +} + +void WaylandClient::updateResourceName() +{ + const QFileInfo fileInfo(surface()->client()->executablePath()); + if (fileInfo.exists()) { + setResourceClass(fileInfo.fileName().toUtf8()); + } +} + +void WaylandClient::updateCaption() +{ + const QString oldSuffix = m_captionSuffix; + const auto shortcut = shortcutCaptionSuffix(); + m_captionSuffix = shortcut; + if ((!isSpecialWindow() || isToolbar()) && findClientWithSameCaption()) { + int i = 2; + do { + m_captionSuffix = shortcut + QLatin1String(" <") + QString::number(i) + QLatin1Char('>'); + i++; + } while (findClientWithSameCaption()); + } + if (m_captionSuffix != oldSuffix) { + emit captionChanged(); + } +} + +void WaylandClient::setCaption(const QString &caption) +{ + const QString oldSuffix = m_captionSuffix; + m_captionNormal = caption.simplified(); + updateCaption(); + if (m_captionSuffix == oldSuffix) { + // Don't emit caption change twice it already got emitted by the changing suffix. + emit captionChanged(); + } +} + +void WaylandClient::doSetActive() +{ + if (isActive()) { // TODO: Xwayland clients must be unfocused somewhere else. + StackingUpdatesBlocker blocker(workspace()); + workspace()->focusToNull(); + } +} + +} // namespace KWin diff --git a/waylandclient.h b/waylandclient.h new file mode 100644 index 000000000..1c140a62d --- /dev/null +++ b/waylandclient.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2020 Vlad Zahorodnii + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General 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_client.h" + +namespace KWin +{ + +class WaylandClient : public AbstractClient +{ + Q_OBJECT + +public: + WaylandClient(KWaylandServer::SurfaceInterface *surface); + + QString captionNormal() const override; + QString captionSuffix() const override; + QStringList activities() const override; + void setOnAllActivities(bool set) override; + void blockActivityUpdates(bool b = true) override; + QPoint clientContentPos() const override; + QRect transparentRect() const override; + quint32 windowId() const override; + pid_t pid() const override; + bool isLockScreen() const override; + bool isInputMethod() const override; + bool isLocalhost() const override; + double opacity() const override; + void setOpacity(double opacity) override; + AbstractClient *findModal(bool allow_itself = false) override; + void resizeWithChecks(const QSize &size, ForceGeometry_t force = NormalGeometrySet) override; + void killWindow() override; + QByteArray windowRole() const override; + + void setCaption(const QString &caption); + +protected: + bool belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const override; + bool belongsToDesktop() const override; + void doSetActive() override; + void updateCaption() override; + +private: + void updateClientArea(); + void updateClientOutputs(); + void updateIcon(); + void updateResourceName(); + + QString m_captionNormal; + QString m_captionSuffix; + double m_opacity = 1.0; + quint32 m_windowId; +}; + +} // namespace KWin diff --git a/xdgshellclient.cpp b/xdgshellclient.cpp index cefdc6e15..c3556b47a 100644 --- a/xdgshellclient.cpp +++ b/xdgshellclient.cpp @@ -1,2025 +1,2153 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin Copyright (C) 2018 David Edmundson Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ #include "xdgshellclient.h" -#include "cursor.h" -#include "decorations/decoratedclient.h" -#include "decorations/decorationbridge.h" #include "deleted.h" -#include "placement.h" #include "screenedge.h" #include "screens.h" +#include "subsurfacemonitor.h" +#include "wayland_server.h" +#include "workspace.h" + #ifdef KWIN_BUILD_TABBOX #include "tabbox.h" #endif -#include "virtualdesktops.h" -#include "wayland_server.h" -#include "workspace.h" #include #include #include #include -#include -#include +#include #include -#include #include #include #include -#include #include -#include - -#include - -#include -#include - -#include - -Q_DECLARE_METATYPE(NET::WindowType) +#include +#include using namespace KWaylandServer; namespace KWin { -XdgShellClient::XdgShellClient(XdgShellSurfaceInterface *surface) - : AbstractClient() - , m_xdgShellToplevel(surface) - , m_xdgShellPopup(nullptr) -{ - setSurface(surface->surface()); - init(); -} - -XdgShellClient::XdgShellClient(XdgShellPopupInterface *surface) - : AbstractClient() - , m_xdgShellToplevel(nullptr) - , m_xdgShellPopup(surface) -{ - setSurface(surface->surface()); - init(); -} - -XdgShellClient::~XdgShellClient() = default; - -void XdgShellClient::init() +XdgSurfaceClient::XdgSurfaceClient(XdgSurfaceInterface *shellSurface) + : WaylandClient(shellSurface->surface()) + , m_shellSurface(shellSurface) + , m_configureTimer(new QTimer(this)) { - m_requestGeometryBlockCounter++; - - connect(this, &XdgShellClient::desktopFileNameChanged, this, &XdgShellClient::updateIcon); - createWindowId(); setupCompositing(); - updateIcon(); - - // TODO: Initialize with null rect. - m_frameGeometry = QRect(0, 0, -1, -1); - m_windowGeometry = QRect(0, 0, -1, -1); - - if (waylandServer()->inputMethodConnection() == surface()->client()) { - m_windowType = NET::OnScreenDisplay; - } - - connect(surface(), &SurfaceInterface::unmapped, this, &XdgShellClient::unmap); - connect(surface(), &SurfaceInterface::unbound, this, &XdgShellClient::destroyClient); - connect(surface(), &SurfaceInterface::destroyed, this, &XdgShellClient::destroyClient); - - if (m_xdgShellToplevel) { - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::destroyed, this, &XdgShellClient::destroyClient); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::configureAcknowledged, this, &XdgShellClient::handleConfigureAcknowledged); - m_caption = m_xdgShellToplevel->title().simplified(); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::titleChanged, this, &XdgShellClient::handleWindowTitleChanged); - QTimer::singleShot(0, this, &XdgShellClient::updateCaption); + connect(shellSurface, &XdgSurfaceInterface::configureAcknowledged, + this, &XdgSurfaceClient::handleConfigureAcknowledged); + connect(shellSurface->surface(), &SurfaceInterface::committed, + this, &XdgSurfaceClient::handleCommit); + connect(shellSurface->surface(), &SurfaceInterface::shadowChanged, + this, &XdgSurfaceClient::updateShadow); + connect(shellSurface->surface(), &SurfaceInterface::unmapped, + this, &XdgSurfaceClient::internalUnmap); + connect(shellSurface->surface(), &SurfaceInterface::unbound, + this, &XdgSurfaceClient::destroyClient); + connect(shellSurface->surface(), &SurfaceInterface::destroyed, + this, &XdgSurfaceClient::destroyClient); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::moveRequested, this, &XdgShellClient::handleMoveRequested); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::resizeRequested, this, &XdgShellClient::handleResizeRequested); - - // Determine the resource name, this is inspired from ICCCM 4.1.2.5 - // the binary name of the invoked client. - QFileInfo info{m_xdgShellToplevel->client()->executablePath()}; - QByteArray resourceName; - if (info.exists()) { - resourceName = info.fileName().toUtf8(); - } - setResourceClass(resourceName, m_xdgShellToplevel->windowClass()); - setDesktopFileName(m_xdgShellToplevel->windowClass()); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::windowClassChanged, this, &XdgShellClient::handleWindowClassChanged); - - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::minimizeRequested, this, &XdgShellClient::handleMinimizeRequested); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::maximizedChanged, this, &XdgShellClient::handleMaximizeRequested); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::fullscreenChanged, this, &XdgShellClient::handleFullScreenRequested); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::windowMenuRequested, this, &XdgShellClient::handleWindowMenuRequested); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::transientForChanged, this, &XdgShellClient::handleTransientForChanged); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::windowGeometryChanged, this, &XdgShellClient::handleWindowGeometryChanged); - - auto global = static_cast(m_xdgShellToplevel->global()); - connect(global, &XdgShellInterface::pingDelayed, this, &XdgShellClient::handlePingDelayed); - connect(global, &XdgShellInterface::pingTimeout, this, &XdgShellClient::handlePingTimeout); - connect(global, &XdgShellInterface::pongReceived, this, &XdgShellClient::handlePongReceived); - - auto configure = [this] { - if (m_closing) { - return; - } - if (m_requestGeometryBlockCounter != 0 || areGeometryUpdatesBlocked()) { - return; - } - m_xdgShellToplevel->configure(xdgSurfaceStates(), m_requestedClientSize); - }; - connect(this, &AbstractClient::activeChanged, this, configure); - connect(this, &AbstractClient::clientStartUserMovedResized, this, configure); - connect(this, &AbstractClient::clientFinishUserMovedResized, this, configure); + // The effective window geometry is determined by two things: (a) the rectangle that bounds + // the main surface and all of its sub-surfaces, (b) the client-specified window geometry, if + // any. If the client hasn't provided the window geometry, we fallback to the bounding sub- + // surface rectangle. If the client has provided the window geometry, we intersect it with + // the bounding rectangle and that will be the effective window geometry. It's worth to point + // out that geometry updates do not occur that frequently, so we don't need to recompute the + // bounding geometry every time the client commits the surface. - connect(this, &XdgShellClient::frameGeometryChanged, this, &XdgShellClient::updateClientOutputs); - connect(screens(), &Screens::changed, this, &XdgShellClient::updateClientOutputs); - } else if (m_xdgShellPopup) { - connect(m_xdgShellPopup, &XdgShellPopupInterface::configureAcknowledged, this, &XdgShellClient::handleConfigureAcknowledged); - connect(m_xdgShellPopup, &XdgShellPopupInterface::grabRequested, this, &XdgShellClient::handleGrabRequested); - connect(m_xdgShellPopup, &XdgShellPopupInterface::destroyed, this, &XdgShellClient::destroyClient); - connect(m_xdgShellPopup, &XdgShellPopupInterface::windowGeometryChanged, this, &XdgShellClient::handleWindowGeometryChanged); - } + SubSurfaceMonitor *treeMonitor = new SubSurfaceMonitor(surface(), this); - // set initial desktop - setDesktop(VirtualDesktopManager::self()->current()); + connect(treeMonitor, &SubSurfaceMonitor::subSurfaceAdded, + this, &XdgSurfaceClient::setHaveNextWindowGeometry); + connect(treeMonitor, &SubSurfaceMonitor::subSurfaceRemoved, + this, &XdgSurfaceClient::setHaveNextWindowGeometry); + connect(treeMonitor, &SubSurfaceMonitor::subSurfaceMoved, + this, &XdgSurfaceClient::setHaveNextWindowGeometry); + connect(treeMonitor, &SubSurfaceMonitor::subSurfaceResized, + this, &XdgSurfaceClient::setHaveNextWindowGeometry); + connect(shellSurface, &XdgSurfaceInterface::windowGeometryChanged, + this, &XdgSurfaceClient::setHaveNextWindowGeometry); + connect(surface(), &SurfaceInterface::sizeChanged, + this, &XdgSurfaceClient::setHaveNextWindowGeometry); - // setup shadow integration - updateShadow(); - connect(surface(), &SurfaceInterface::shadowChanged, this, &Toplevel::updateShadow); + // Configure events are not sent immediately, but rather scheduled to be sent when the event + // loop is about to be idle. By doing this, we can avoid sending configure events that do + // nothing, and implementation-wise, it's simpler. - connect(waylandServer(), &WaylandServer::foreignTransientChanged, this, [this](KWaylandServer::SurfaceInterface *child) { - if (child == surface()) { - handleTransientForChanged(); - } - }); - handleTransientForChanged(); + m_configureTimer->setSingleShot(true); + connect(m_configureTimer, &QTimer::timeout, this, &XdgSurfaceClient::sendConfigure); - AbstractClient::updateColorScheme(QString()); + // Unfortunately, AbstractClient::checkWorkspacePosition() operates on the geometry restore + // so we need to initialize it with some reasonable value; otherwise bad things will happen + // when we want to decorate the client or move the client to another screen. This is a hack. - connect(surface(), &SurfaceInterface::committed, this, &XdgShellClient::finishInit); + connect(this, &XdgSurfaceClient::frameGeometryChanged, + this, &XdgSurfaceClient::updateGeometryRestoreHack); } -void XdgShellClient::finishInit() +XdgSurfaceClient::~XdgSurfaceClient() { - disconnect(surface(), &SurfaceInterface::committed, this, &XdgShellClient::finishInit); - - connect(surface(), &SurfaceInterface::committed, this, &XdgShellClient::handleCommitted); - - bool needsPlacement = !isInitialPositionSet(); - - if (supportsWindowRules()) { - setupWindowRules(false); - - const QRect originalGeometry = frameGeometry(); - const QRect ruledGeometry = rules()->checkGeometry(originalGeometry, true); - if (originalGeometry != ruledGeometry) { - setFrameGeometry(ruledGeometry); - } - - maximize(rules()->checkMaximize(maximizeMode(), true)); - - setDesktop(rules()->checkDesktop(desktop(), true)); - setDesktopFileName(rules()->checkDesktopFile(desktopFileName(), true).toUtf8()); - if (rules()->checkMinimize(isMinimized(), true)) { - minimize(true); // No animation. - } - setSkipTaskbar(rules()->checkSkipTaskbar(skipTaskbar(), true)); - setSkipPager(rules()->checkSkipPager(skipPager(), true)); - setSkipSwitcher(rules()->checkSkipSwitcher(skipSwitcher(), true)); - setKeepAbove(rules()->checkKeepAbove(keepAbove(), true)); - setKeepBelow(rules()->checkKeepBelow(keepBelow(), true)); - setShortcut(rules()->checkShortcut(shortcut().toString(), true)); - updateColorScheme(); - - // Don't place the client if its position is set by a rule. - if (rules()->checkPosition(invalidPoint, true) != invalidPoint) { - needsPlacement = false; - } - - // Don't place the client if the maximize state is set by a rule. - if (requestedMaximizeMode() != MaximizeRestore) { - needsPlacement = false; - } - - discardTemporaryRules(); - RuleBook::self()->discardUsed(this, false); // Remove Apply Now rules. - updateWindowRules(Rules::All); - } - - if (isFullScreen()) { - needsPlacement = false; - } - - if (needsPlacement) { - const QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop()); - placeIn(area); - } - - m_requestGeometryBlockCounter--; - if (m_requestGeometryBlockCounter == 0) { - requestGeometry(m_blockedRequestGeometry); - } - - m_isInitialized = true; + qDeleteAll(m_configureEvents); } -void XdgShellClient::destroyClient() +QRect XdgSurfaceClient::requestedFrameGeometry() const { - m_closing = true; -#ifdef KWIN_BUILD_TABBOX - TabBox::TabBox *tabBox = TabBox::TabBox::self(); - if (tabBox->isDisplayed() && tabBox->currentClient() == this) { - tabBox->nextPrev(true); - } -#endif - if (isMoveResize()) { - leaveMoveResize(); - } - - // Replace ShellClient with an instance of Deleted in the stacking order. - Deleted *deleted = Deleted::create(this); - emit windowClosed(this, deleted); - - // Remove Force Temporarily rules. - RuleBook::self()->discardUsed(this, true); - - destroyWindowManagementInterface(); - destroyDecoration(); - - StackingUpdatesBlocker blocker(workspace()); - if (transientFor()) { - transientFor()->removeTransient(this); - } - 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; - } - } - - waylandServer()->removeClient(this); - - deleted->unrefWindow(); - - m_xdgShellToplevel = nullptr; - m_xdgShellPopup = nullptr; - deleteClient(this); + return m_requestedFrameGeometry; } -void XdgShellClient::deleteClient(XdgShellClient *c) +QPoint XdgSurfaceClient::requestedPos() const { - delete c; + return m_requestedFrameGeometry.topLeft(); } -QRect XdgShellClient::inputGeometry() const +QSize XdgSurfaceClient::requestedSize() const { - if (isDecorated()) { - return AbstractClient::inputGeometry(); - } - // TODO: What about sub-surfaces sticking outside the main surface? - return m_bufferGeometry; + return m_requestedFrameGeometry.size(); } -QRect XdgShellClient::bufferGeometry() const +QRect XdgSurfaceClient::requestedClientGeometry() const { - return m_bufferGeometry; + return m_requestedClientGeometry; } -QStringList XdgShellClient::activities() const +QRect XdgSurfaceClient::inputGeometry() const { - // TODO: implement - return QStringList(); + return isDecorated() ? AbstractClient::inputGeometry() : bufferGeometry(); } -QPoint XdgShellClient::clientContentPos() const +QRect XdgSurfaceClient::bufferGeometry() const { - return -1 * clientPos(); + return m_bufferGeometry; } -QSize XdgShellClient::clientSize() const +QSize XdgSurfaceClient::requestedClientSize() const { - const QRect boundingRect = surface()->boundingRect(); - return m_windowGeometry.size().boundedTo(boundingRect.size()); + return requestedClientGeometry().size(); } -QSize XdgShellClient::minSize() const +QRect XdgSurfaceClient::clientGeometry() const { - if (m_xdgShellToplevel) { - return rules()->checkMinSize(m_xdgShellToplevel->minimumSize()); - } - return QSize(0, 0); + return m_clientGeometry; } -QSize XdgShellClient::maxSize() const +QSize XdgSurfaceClient::clientSize() const { - if (m_xdgShellToplevel) { - return rules()->checkMaxSize(m_xdgShellToplevel->maximumSize()); - } - return QSize(INT_MAX, INT_MAX); + return m_clientGeometry.size(); } -void XdgShellClient::debug(QDebug &stream) const +QMatrix4x4 XdgSurfaceClient::inputTransformation() const { - stream.nospace(); - stream << "\'XdgShellClient:" << surface() << ";WMCLASS:" << resourceClass() << ":" - << resourceName() << ";Caption:" << caption() << "\'"; + QMatrix4x4 transformation; + transformation.translate(-m_bufferGeometry.x(), -m_bufferGeometry.y()); + return transformation; } -bool XdgShellClient::belongsToDesktop() const +XdgSurfaceConfigure *XdgSurfaceClient::lastAcknowledgedConfigure() const { - const auto clients = waylandServer()->clients(); - - return std::any_of(clients.constBegin(), clients.constEnd(), - [this](const AbstractClient *client) { - if (belongsToSameApplication(client, SameApplicationChecks())) { - return client->isDesktop(); - } - return false; - } - ); + return m_lastAcknowledgedConfigure.data(); } -Layer XdgShellClient::layerForDock() const +bool XdgSurfaceClient::stateCompare() const { - if (m_plasmaShellSurface) { - switch (m_plasmaShellSurface->panelBehavior()) { - case PlasmaShellSurfaceInterface::PanelBehavior::WindowsCanCover: - return NormalLayer; - case PlasmaShellSurfaceInterface::PanelBehavior::AutoHide: - return AboveLayer; - case PlasmaShellSurfaceInterface::PanelBehavior::WindowsGoBelow: - case PlasmaShellSurfaceInterface::PanelBehavior::AlwaysVisible: - return DockLayer; - default: - Q_UNREACHABLE(); - break; - } + if (m_requestedFrameGeometry != m_frameGeometry) { + return true; } - return AbstractClient::layerForDock(); -} - -QRect XdgShellClient::transparentRect() const -{ - // TODO: implement - return QRect(); -} - -NET::WindowType XdgShellClient::windowType(bool direct, int supported_types) const -{ - // TODO: implement - Q_UNUSED(direct) - Q_UNUSED(supported_types) - return m_windowType; -} - -double XdgShellClient::opacity() const -{ - return m_opacity; + if (m_requestedClientGeometry != m_clientGeometry) { + return true; + } + if (m_requestedClientGeometry.isEmpty()) { + return true; + } + return false; } -void XdgShellClient::setOpacity(double opacity) +void XdgSurfaceClient::scheduleConfigure() { - const qreal newOpacity = qBound(0.0, opacity, 1.0); - if (newOpacity == m_opacity) { + if (isClosing()) { return; } - const qreal oldOpacity = m_opacity; - m_opacity = newOpacity; - addRepaintFull(); - emit opacityChanged(this, oldOpacity); -} - -void XdgShellClient::addDamage(const QRegion &damage) -{ - const int offsetX = m_bufferGeometry.x() - frameGeometry().x(); - const int offsetY = m_bufferGeometry.y() - frameGeometry().y(); - repaints_region += damage.translated(offsetX, offsetY); - Toplevel::addDamage(damage); + if (stateCompare()) { + m_configureTimer->start(); + } else { + m_configureTimer->stop(); + } } -void XdgShellClient::markAsMapped() +void XdgSurfaceClient::sendConfigure() { - if (!m_unmapped) { - return; - } + XdgSurfaceConfigure *configureEvent = sendRoleConfigure(); - m_unmapped = false; - if (!ready_for_painting) { - setReadyForPainting(); - } else { - addRepaintFull(); - emit windowShown(this); + if (configureEvent->position != pos()) { + configureEvent->presentFields |= XdgSurfaceConfigure::PositionField; } - if (shouldExposeToWindowManagement()) { - setupWindowManagementInterface(); + if (configureEvent->size != size()) { + configureEvent->presentFields |= XdgSurfaceConfigure::SizeField; } - updateShowOnScreenEdge(); + + m_configureEvents.append(configureEvent); } -void XdgShellClient::updateDecoration(bool check_workspace_pos, bool force) +void XdgSurfaceClient::handleConfigureAcknowledged(quint32 serial) { - if (!force && - ((!isDecorated() && noBorder()) || (isDecorated() && !noBorder()))) - return; - QRect oldgeom = frameGeometry(); - QRect oldClientGeom = oldgeom.adjusted(borderLeft(), borderTop(), -borderRight(), -borderBottom()); - blockGeometryUpdates(true); - if (force) - destroyDecoration(); - if (!noBorder()) { - createDecoration(oldgeom); - } else - destroyDecoration(); - if (m_serverDecoration && isDecorated()) { - m_serverDecoration->setMode(KWaylandServer::ServerSideDecorationManagerInterface::Mode::Server); - } - if (m_xdgDecoration) { - auto mode = isDecorated() || m_userNoBorder ? XdgDecorationInterface::Mode::ServerSide: XdgDecorationInterface::Mode::ClientSide; - m_xdgDecoration->configure(mode); - if (m_requestGeometryBlockCounter == 0) { - m_xdgShellToplevel->configure(xdgSurfaceStates(), m_requestedClientSize); + while (!m_configureEvents.isEmpty()) { + if (serial < m_configureEvents.first()->serial) { + break; } + m_lastAcknowledgedConfigure.reset(m_configureEvents.takeFirst()); } - updateShadow(); - if (check_workspace_pos) - checkWorkspacePosition(oldgeom, -2, oldClientGeom); - blockGeometryUpdates(false); } -void XdgShellClient::setFrameGeometry(const QRect &rect, ForceGeometry_t force) +void XdgSurfaceClient::handleCommit() { - const QRect newGeometry = rules()->checkGeometry(rect); - - if (areGeometryUpdatesBlocked()) { - // when the GeometryUpdateBlocker exits the current geom is passed to setGeometry - // thus we need to set it here. - m_frameGeometry = newGeometry; - if (pendingGeometryUpdate() == PendingGeometryForced) { - // maximum, nothing needed - } else if (force == ForceGeometrySet) { - setPendingGeometryUpdate(PendingGeometryForced); - } else { - setPendingGeometryUpdate(PendingGeometryNormal); - } + if (!surface()->buffer()) { return; } - if (pendingGeometryUpdate() != PendingGeometryNone) { - // reset geometry to the one before blocking, so that we can compare properly - m_frameGeometry = frameGeometryBeforeUpdateBlocking(); + if (haveNextWindowGeometry()) { + handleNextWindowGeometry(); + resetHaveNextWindowGeometry(); } - const QSize requestedClientSize = newGeometry.size() - QSize(borderLeft() + borderRight(), borderTop() + borderBottom()); + handleRoleCommit(); + m_lastAcknowledgedConfigure.reset(); - if (requestedClientSize == m_windowGeometry.size() && - (m_requestedClientSize.isEmpty() || requestedClientSize == m_requestedClientSize)) { - // size didn't change, and we don't need to explicitly request a new size - doSetGeometry(newGeometry); - updateMaximizeMode(m_requestedMaximizeMode); - } else { - // size did change, Client needs to provide a new buffer - requestGeometry(newGeometry); - } + internalMap(); + updateDepth(); } -QRect XdgShellClient::determineBufferGeometry() const +void XdgSurfaceClient::handleRoleCommit() { - // Offset of the main surface relative to the frame rect. - const int offsetX = borderLeft() - m_windowGeometry.left(); - const int offsetY = borderTop() - m_windowGeometry.top(); - - QRect bufferGeometry; - bufferGeometry.setX(x() + offsetX); - bufferGeometry.setY(y() + offsetY); - bufferGeometry.setSize(surface()->size()); - - return bufferGeometry; } -void XdgShellClient::doSetGeometry(const QRect &rect) +void XdgSurfaceClient::handleNextWindowGeometry() { - bool frameGeometryIsChanged = false; - bool bufferGeometryIsChanged = false; + const QRect boundingGeometry = surface()->boundingRect(); - if (m_frameGeometry != rect) { - m_frameGeometry = rect; - frameGeometryIsChanged = true; - } + // The effective window geometry is defined as the intersection of the window geometry + // and the rectangle that bounds the main surface and all of its sub-surfaces. If the + // client hasn't specified the window geometry, we must fallback to the bounding geometry. + // Note that the xdg-shell spec is not clear about when exactly we have to clamp the + // window geometry. - const QRect bufferGeometry = determineBufferGeometry(); - if (m_bufferGeometry != bufferGeometry) { - m_bufferGeometry = bufferGeometry; - bufferGeometryIsChanged = true; + m_windowGeometry = m_shellSurface->windowGeometry(); + if (m_windowGeometry.isValid()) { + m_windowGeometry &= boundingGeometry; + } else { + m_windowGeometry = boundingGeometry; } - if (!frameGeometryIsChanged && !bufferGeometryIsChanged) { - return; + if (m_windowGeometry.isEmpty()) { + qCWarning(KWIN_CORE) << "Committed empty window geometry, dealing with a buggy client!"; } - if (m_unmapped && geometryRestore().isEmpty() && !m_frameGeometry.isEmpty()) { - // use first valid geometry as restore geometry - setGeometryRestore(m_frameGeometry); - } + QRect frameGeometry(pos(), clientSizeToFrameSize(m_windowGeometry.size())); - if (frameGeometryIsChanged) { - if (hasStrut()) { - workspace()->updateClientArea(); + // We're not done yet. The xdg-shell spec allows clients to attach buffers smaller than + // we asked. Normally, this is not a big deal, but when the client is being interactively + // resized, it may cause the window contents to bounce. In order to counter this, we have + // to "gravitate" the new geometry according to the current move-resize pointer mode so + // the opposite window corner stays still. + + if (isMoveResize()) { + frameGeometry = adjustMoveResizeGeometry(frameGeometry); + } else if (lastAcknowledgedConfigure()) { + XdgSurfaceConfigure *configureEvent = lastAcknowledgedConfigure(); + + if (configureEvent->presentFields & XdgSurfaceConfigure::PositionField) { + frameGeometry.moveTopLeft(configureEvent->position); } - updateWindowRules(Rules::Position | Rules::Size); - emit frameGeometryChanged(this, frameGeometryBeforeUpdateBlocking()); } - emit geometryShapeChanged(this, frameGeometryBeforeUpdateBlocking()); - - addRepaintDuringGeometryUpdates(); - updateGeometryBeforeUpdateBlocking(); + updateGeometry(frameGeometry); if (isResize()) { performMoveResize(); } } -void XdgShellClient::doMove(int x, int y) +bool XdgSurfaceClient::haveNextWindowGeometry() const { - Q_UNUSED(x) - Q_UNUSED(y) - m_bufferGeometry = determineBufferGeometry(); + return m_haveNextWindowGeometry || m_lastAcknowledgedConfigure; } -QByteArray XdgShellClient::windowRole() const +void XdgSurfaceClient::setHaveNextWindowGeometry() { - return QByteArray(); + m_haveNextWindowGeometry = true; } -bool XdgShellClient::belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const +void XdgSurfaceClient::resetHaveNextWindowGeometry() { - if (checks.testFlag(SameApplicationCheck::AllowCrossProcesses)) { - if (other->desktopFileName() == desktopFileName()) { - return true; - } - } - if (auto s = other->surface()) { - return s->client() == surface()->client(); - } - return false; + m_haveNextWindowGeometry = false; } -void XdgShellClient::blockActivityUpdates(bool b) +QRect XdgSurfaceClient::adjustMoveResizeGeometry(const QRect &rect) const { - Q_UNUSED(b) -} + QRect geometry = rect; -QString XdgShellClient::captionNormal() const -{ - return m_caption; -} + switch (moveResizePointerMode()) { + case PositionTopLeft: + geometry.moveRight(moveResizeGeometry().right()); + geometry.moveBottom(moveResizeGeometry().bottom()); + break; + case PositionTop: + case PositionTopRight: + geometry.moveLeft(moveResizeGeometry().left()); + geometry.moveBottom(moveResizeGeometry().bottom()); + break; + case PositionRight: + case PositionBottomRight: + case PositionBottom: + case PositionCenter: + geometry.moveLeft(moveResizeGeometry().left()); + geometry.moveTop(moveResizeGeometry().top()); + break; + case PositionBottomLeft: + case PositionLeft: + geometry.moveRight(moveResizeGeometry().right()); + geometry.moveTop(moveResizeGeometry().top()); + break; + } -QString XdgShellClient::captionSuffix() const -{ - return m_captionSuffix; + return geometry; } -void XdgShellClient::updateCaption() -{ - const QString oldSuffix = m_captionSuffix; - const auto shortcut = shortcutCaptionSuffix(); - m_captionSuffix = shortcut; - if ((!isSpecialWindow() || isToolbar()) && findClientWithSameCaption()) { - int i = 2; - do { - m_captionSuffix = shortcut + QLatin1String(" <") + QString::number(i) + QLatin1Char('>'); - i++; - } while (findClientWithSameCaption()); +/** + * Sets the frame geometry of the XdgSurfaceClient to \a rect. + * + * Because geometry updates are asynchronous on Wayland, there are no any guarantees that + * the frame geometry will be changed immediately. We may need to send a configure event to + * the client if the current window geometry size and the requested window geometry size + * don't match. frameGeometryChanged() will be emitted when the requested frame geometry + * has been applied. + * + * Notice that the client may attach a buffer smaller than the one in the configure event. + */ +void XdgSurfaceClient::setFrameGeometry(const QRect &rect, ForceGeometry_t force) +{ + m_requestedFrameGeometry = rect; + + // XdgToplevelClient currently doesn't support shaded clients, but let's stick with + // what X11Client does. Hopefully, one day we will be able to unify setFrameGeometry() + // for all AbstractClient subclasses. It's going to be great! + + if (isShade()) { + if (m_requestedFrameGeometry.height() == borderTop() + borderBottom()) { + qCDebug(KWIN_CORE) << "Passed shaded frame geometry to setFrameGeometry()"; + } else { + m_requestedClientGeometry = frameRectToClientRect(m_requestedFrameGeometry); + m_requestedFrameGeometry.setHeight(borderTop() + borderBottom()); + } + } else { + m_requestedClientGeometry = frameRectToClientRect(m_requestedFrameGeometry); } - if (m_captionSuffix != oldSuffix) { - emit captionChanged(); - } -} -void XdgShellClient::closeWindow() -{ - if (m_xdgShellToplevel && isCloseable()) { - m_xdgShellToplevel->close(); - ping(PingReason::CloseWindow); + if (areGeometryUpdatesBlocked()) { + m_frameGeometry = m_requestedFrameGeometry; + if (pendingGeometryUpdate() == PendingGeometryForced) { + return; + } + if (force == ForceGeometrySet) { + setPendingGeometryUpdate(PendingGeometryForced); + } else { + setPendingGeometryUpdate(PendingGeometryNormal); + } + return; } -} -AbstractClient *XdgShellClient::findModal(bool allow_itself) -{ - Q_UNUSED(allow_itself) - return nullptr; + m_frameGeometry = frameGeometryBeforeUpdateBlocking(); + + // Notice that the window geometry size of (0, 0) has special meaning to xdg shell clients. + // It basically says "pick whatever size you think is the best, dawg." + + if (requestedClientSize() != clientSize()) { + requestGeometry(requestedFrameGeometry()); + } else { + updateGeometry(requestedFrameGeometry()); + } } -bool XdgShellClient::isCloseable() const +void XdgSurfaceClient::move(int x, int y, ForceGeometry_t force) { - if (m_windowType == NET::Desktop || m_windowType == NET::Dock) { - return false; + Q_ASSERT(pendingGeometryUpdate() == PendingGeometryNone || areGeometryUpdatesBlocked()); + QPoint p(x, y); + if (!areGeometryUpdatesBlocked() && p != rules()->checkPosition(p)) { + qCDebug(KWIN_CORE) << "forced position fail:" << p << ":" << rules()->checkPosition(p); } - if (m_xdgShellToplevel) { - return true; + m_requestedFrameGeometry.moveTopLeft(p); + m_requestedClientGeometry.moveTopLeft(framePosToClientPos(p)); + if (force == NormalGeometrySet && m_frameGeometry.topLeft() == p) { + return; } - return false; + m_frameGeometry.moveTopLeft(m_requestedFrameGeometry.topLeft()); + if (areGeometryUpdatesBlocked()) { + if (pendingGeometryUpdate() == PendingGeometryForced) { + return; + } + if (force == ForceGeometrySet) { + setPendingGeometryUpdate(PendingGeometryForced); + } else { + setPendingGeometryUpdate(PendingGeometryNormal); + } + return; + } + m_clientGeometry.moveTopLeft(m_requestedClientGeometry.topLeft()); + m_bufferGeometry = frameRectToBufferRect(m_frameGeometry); + updateWindowRules(Rules::Position); + screens()->setCurrent(this); + workspace()->updateStackingOrder(); + emit frameGeometryChanged(this, frameGeometryBeforeUpdateBlocking()); + addRepaintDuringGeometryUpdates(); + updateGeometryBeforeUpdateBlocking(); } -bool XdgShellClient::isFullScreen() const +void XdgSurfaceClient::requestGeometry(const QRect &rect) { - return m_fullScreen; + m_requestedFrameGeometry = rect; + m_requestedClientGeometry = frameRectToClientRect(rect); + + scheduleConfigure(); // Send the configure event later. } -bool XdgShellClient::isMaximizable() const +void XdgSurfaceClient::updateGeometry(const QRect &rect) { - if (!isResizable()) { - return false; - } - if (rules()->checkMaximize(MaximizeRestore) != MaximizeRestore || rules()->checkMaximize(MaximizeFull) != MaximizeFull) { - return false; + const QRect oldFrameGeometry = m_frameGeometry; + + m_frameGeometry = rect; + m_bufferGeometry = frameRectToBufferRect(rect); + m_clientGeometry = frameRectToClientRect(rect); + + if (oldFrameGeometry == m_frameGeometry) { + return; } - return true; + + updateWindowRules(Rules::Position | Rules::Size); + updateGeometryBeforeUpdateBlocking(); + + emit frameGeometryChanged(this, oldFrameGeometry); + emit geometryShapeChanged(this, oldFrameGeometry); + + addRepaintDuringGeometryUpdates(); } -bool XdgShellClient::isMinimizable() const +/** + * \internal + * \todo We have to check the current frame geometry in checkWorskpacePosition(). + * + * Sets the geometry restore to the first valid frame geometry. This is a HACK! + * + * Unfortunately, AbstractClient::checkWorkspacePosition() operates on the geometry restore + * rather than the current frame geometry, so we have to ensure that it's initialized with + * some reasonable value even if the client is not maximized or quick tiled. + */ +void XdgSurfaceClient::updateGeometryRestoreHack() { - if (!rules()->checkMinimize(true)) { - return false; + if (isUnmapped() && geometryRestore().isEmpty() && !frameGeometry().isEmpty()) { + setGeometryRestore(frameGeometry()); } - return (!m_plasmaShellSurface || m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal); } -bool XdgShellClient::isMovable() const +void XdgSurfaceClient::updateDepth() { - if (isFullScreen()) { - return false; - } - if (rules()->checkPosition(invalidPoint) != invalidPoint) { - return false; - } - if (m_plasmaShellSurface) { - return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal; - } - if (m_xdgShellPopup) { - return false; + if (surface()->buffer()->hasAlphaChannel() && !isDesktop()) { + setDepth(32); + } else { + setDepth(24); } - return true; } -bool XdgShellClient::isMovableAcrossScreens() const +QRect XdgSurfaceClient::frameRectToBufferRect(const QRect &rect) const { - if (rules()->checkPosition(invalidPoint) != invalidPoint) { - return false; - } - if (m_plasmaShellSurface) { - return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal; - } - if (m_xdgShellPopup) { - return false; - } - return true; + const int left = rect.left() + borderLeft() - m_windowGeometry.left(); + const int top = rect.top() + borderTop() - m_windowGeometry.top(); + return QRect(QPoint(left, top), surface()->size()); } -bool XdgShellClient::isResizable() const +void XdgSurfaceClient::addDamage(const QRegion &damage) { - if (isFullScreen()) { - return false; - } - if (rules()->checkSize(QSize()).isValid()) { - return false; - } - if (m_plasmaShellSurface) { - return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal; - } - if (m_xdgShellPopup) { - return false; - } - return true; + const int offsetX = m_bufferGeometry.x() - m_frameGeometry.x(); + const int offsetY = m_bufferGeometry.y() - m_frameGeometry.y(); + repaints_region += damage.translated(offsetX, offsetY); + Toplevel::addDamage(damage); } -bool XdgShellClient::isShown(bool shaded_is_shown) const +bool XdgSurfaceClient::isShown(bool shaded_is_shown) const { Q_UNUSED(shaded_is_shown) - return !m_closing && !m_unmapped && !isMinimized() && !m_hidden; + return !isClosing() && !isHidden() && !isMinimized() && !isUnmapped(); } -bool XdgShellClient::isHiddenInternal() const +bool XdgSurfaceClient::isHiddenInternal() const { - return m_unmapped || m_hidden; + return isHidden() || isUnmapped(); } -void XdgShellClient::hideClient(bool hide) +void XdgSurfaceClient::hideClient(bool hide) { - if (m_hidden == hide) { - return; - } - m_hidden = hide; if (hide) { - addWorkspaceRepaint(visibleRect()); - workspace()->clientHidden(this); - emit windowHidden(this); + internalHide(); } else { - emit windowShown(this); + internalShow(); } } -static bool changeMaximizeRecursion = false; -void XdgShellClient::changeMaximize(bool horizontal, bool vertical, bool adjust) +bool XdgSurfaceClient::isHidden() const { - if (changeMaximizeRecursion) { + return m_isHidden; +} + +void XdgSurfaceClient::internalShow() +{ + if (!isHidden()) { return; } + m_isHidden = false; + addRepaintFull(); + emit windowShown(this); +} - if (!isResizable()) { +void XdgSurfaceClient::internalHide() +{ + if (isHidden()) { return; } + if (isMoveResize()) { + leaveMoveResize(); + } + m_isHidden = true; + addWorkspaceRepaint(visibleRect()); + workspace()->clientHidden(this); + emit windowHidden(this); +} - const QRect clientArea = isElectricBorderMaximizing() ? - workspace()->clientArea(MaximizeArea, Cursors::self()->mouse()->pos(), desktop()) : - workspace()->clientArea(MaximizeArea, this); - - const MaximizeMode oldMode = m_requestedMaximizeMode; - const QRect oldGeometry = frameGeometry(); +/** + * \todo We just need to destroy XdgSurfaceClient when the xdg-surface is unmapped. + */ +bool XdgSurfaceClient::isUnmapped() const +{ + return m_isUnmapped; +} - // 'adjust == true' means to update the size only, e.g. after changing workspace size - if (!adjust) { - if (vertical) - m_requestedMaximizeMode = MaximizeMode(m_requestedMaximizeMode ^ MaximizeVertical); - if (horizontal) - m_requestedMaximizeMode = MaximizeMode(m_requestedMaximizeMode ^ MaximizeHorizontal); +/** + * \todo We just need to destroy XdgSurfaceClient when the xdg-surface is unmapped. + */ +void XdgSurfaceClient::internalMap() +{ + if (!isUnmapped()) { + return; + } + m_isUnmapped = false; + if (readyForPainting()) { + addRepaintFull(); + emit windowShown(this); + } else { + setReadyForPainting(); } + emit windowMapped(); +} - m_requestedMaximizeMode = rules()->checkMaximize(m_requestedMaximizeMode); - if (!adjust && m_requestedMaximizeMode == oldMode) { +/** + * \todo We just need to destroy XdgSurfaceClient when the xdg-surface is unmapped. + */ +void XdgSurfaceClient::internalUnmap() +{ + if (isUnmapped()) { return; } + if (isMoveResize()) { + leaveMoveResize(); + } + m_isUnmapped = true; + m_requestedClientGeometry = QRect(); + m_lastAcknowledgedConfigure = nullptr; + m_configureTimer->stop(); + qDeleteAll(m_configureEvents); + m_configureEvents.clear(); + addWorkspaceRepaint(visibleRect()); + workspace()->clientHidden(this); + emit windowHidden(this); + emit windowUnmapped(); +} + +bool XdgSurfaceClient::isClosing() const +{ + return m_isClosing; +} +void XdgSurfaceClient::destroyClient() +{ + m_isClosing = true; + m_configureTimer->stop(); + if (isMoveResize()) { + leaveMoveResize(); + } + cleanTabBox(); + Deleted *deleted = Deleted::create(this); + emit windowClosed(this, deleted); StackingUpdatesBlocker blocker(workspace()); - RequestGeometryBlocker geometryBlocker(this); + RuleBook::self()->discardUsed(this, true); + destroyWindowManagementInterface(); + destroyDecoration(); + cleanGrouping(); + waylandServer()->removeClient(this); + deleted->unrefWindow(); + delete this; +} - // call into decoration update borders - if (isDecorated() && decoration()->client() && !(options->borderlessMaximizedWindows() && m_requestedMaximizeMode == KWin::MaximizeFull)) { - changeMaximizeRecursion = true; - const auto c = decoration()->client().toStrongRef(); - if ((m_requestedMaximizeMode & MaximizeVertical) != (oldMode & MaximizeVertical)) { - emit c->maximizedVerticallyChanged(m_requestedMaximizeMode & MaximizeVertical); - } - if ((m_requestedMaximizeMode & MaximizeHorizontal) != (oldMode & MaximizeHorizontal)) { - emit c->maximizedHorizontallyChanged(m_requestedMaximizeMode & MaximizeHorizontal); - } - if ((m_requestedMaximizeMode == MaximizeFull) != (oldMode == MaximizeFull)) { - emit c->maximizedChanged(m_requestedMaximizeMode & MaximizeFull); +void XdgSurfaceClient::cleanGrouping() +{ + if (transientFor()) { + transientFor()->removeTransient(this); + } + 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; } - 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(m_requestedMaximizeMode == MaximizeFull)); - changeMaximizeRecursion = false; +void XdgSurfaceClient::cleanTabBox() +{ +#ifdef KWIN_BUILD_TABBOX + TabBox::TabBox *tabBox = TabBox::TabBox::self(); + if (tabBox->isDisplayed() && tabBox->currentClient() == this) { + tabBox->nextPrev(true); } +#endif +} - // Conditional quick tiling exit points - const auto oldQuickTileMode = quickTileMode(); - if (quickTileMode() != QuickTileMode(QuickTileFlag::None)) { - if (oldMode == MaximizeFull && - !clientArea.contains(geometryRestore().center())) { - // Not restoring on the same screen - // TODO: The following doesn't work for some reason - //quick_tile_mode = QuickTileNone; // And exit quick tile mode manually - } else if ((oldMode == MaximizeVertical && m_requestedMaximizeMode == MaximizeRestore) || - (oldMode == MaximizeFull && m_requestedMaximizeMode == MaximizeHorizontal)) { - // Modifying geometry of a tiled window - updateQuickTileMode(QuickTileFlag::None); // Exit quick tile mode without restoring geometry - } +XdgToplevelClient::XdgToplevelClient(XdgToplevelInterface *shellSurface) + : XdgSurfaceClient(shellSurface->xdgSurface()) + , m_shellSurface(shellSurface) +{ + setupWindowManagementIntegration(); + setupPlasmaShellIntegration(); + setDesktop(VirtualDesktopManager::self()->current()); + + if (waylandServer()->inputMethodConnection() == surface()->client()) { + m_windowType = NET::OnScreenDisplay; } - if (m_requestedMaximizeMode == MaximizeFull) { - setGeometryRestore(oldGeometry); - // TODO: Client has more checks - if (options->electricBorderMaximize()) { - updateQuickTileMode(QuickTileFlag::Maximize); - } else { - updateQuickTileMode(QuickTileFlag::None); - } - if (quickTileMode() != oldQuickTileMode) { - emit quickTileModeChanged(); - } - setFrameGeometry(workspace()->clientArea(MaximizeArea, this)); - workspace()->raiseClient(this); - } else { - if (m_requestedMaximizeMode == MaximizeRestore) { - updateQuickTileMode(QuickTileFlag::None); - } - if (quickTileMode() != oldQuickTileMode) { - emit quickTileModeChanged(); - } + connect(shellSurface, &XdgToplevelInterface::windowTitleChanged, + this, &XdgToplevelClient::handleWindowTitleChanged); + connect(shellSurface, &XdgToplevelInterface::windowClassChanged, + this, &XdgToplevelClient::handleWindowClassChanged); + connect(shellSurface, &XdgToplevelInterface::windowMenuRequested, + this, &XdgToplevelClient::handleWindowMenuRequested); + connect(shellSurface, &XdgToplevelInterface::moveRequested, + this, &XdgToplevelClient::handleMoveRequested); + connect(shellSurface, &XdgToplevelInterface::resizeRequested, + this, &XdgToplevelClient::handleResizeRequested); + connect(shellSurface, &XdgToplevelInterface::maximizeRequested, + this, &XdgToplevelClient::handleMaximizeRequested); + connect(shellSurface, &XdgToplevelInterface::unmaximizeRequested, + this, &XdgToplevelClient::handleUnmaximizeRequested); + connect(shellSurface, &XdgToplevelInterface::fullscreenRequested, + this, &XdgToplevelClient::handleFullscreenRequested); + connect(shellSurface, &XdgToplevelInterface::unfullscreenRequested, + this, &XdgToplevelClient::handleUnfullscreenRequested); + connect(shellSurface, &XdgToplevelInterface::minimizeRequested, + this, &XdgToplevelClient::handleMinimizeRequested); + connect(shellSurface, &XdgToplevelInterface::parentXdgToplevelChanged, + this, &XdgToplevelClient::handleTransientForChanged); + connect(shellSurface, &XdgToplevelInterface::initializeRequested, + this, &XdgToplevelClient::initialize); + connect(shellSurface, &XdgToplevelInterface::destroyed, + this, &XdgToplevelClient::destroyClient); + connect(shellSurface->shell(), &XdgShellInterface::pingTimeout, + this, &XdgToplevelClient::handlePingTimeout); + connect(shellSurface->shell(), &XdgShellInterface::pingDelayed, + this, &XdgToplevelClient::handlePingDelayed); + connect(shellSurface->shell(), &XdgShellInterface::pongReceived, + this, &XdgToplevelClient::handlePongReceived); - if (geometryRestore().isValid()) { - setFrameGeometry(geometryRestore()); - } else { - setFrameGeometry(workspace()->clientArea(PlacementArea, this)); - } - } + connect(waylandServer(), &WaylandServer::foreignTransientChanged, + this, &XdgToplevelClient::handleForeignTransientForChanged); +} + +XdgToplevelClient::~XdgToplevelClient() +{ +} + +void XdgToplevelClient::debug(QDebug &stream) const +{ + stream << this; } -MaximizeMode XdgShellClient::maximizeMode() const +NET::WindowType XdgToplevelClient::windowType(bool direct, int supported_types) const +{ + Q_UNUSED(direct) + Q_UNUSED(supported_types) + return m_windowType; +} + +MaximizeMode XdgToplevelClient::maximizeMode() const { return m_maximizeMode; } -MaximizeMode XdgShellClient::requestedMaximizeMode() const +MaximizeMode XdgToplevelClient::requestedMaximizeMode() const { return m_requestedMaximizeMode; } -bool XdgShellClient::noBorder() const +QSize XdgToplevelClient::minSize() const { - if (m_serverDecoration) { - if (m_serverDecoration->mode() == ServerSideDecorationManagerInterface::Mode::Server) { - return m_userNoBorder || isFullScreen(); - } + return rules()->checkMinSize(m_shellSurface->minimumSize()); +} + +QSize XdgToplevelClient::maxSize() const +{ + return rules()->checkMaxSize(m_shellSurface->maximumSize()); +} + +bool XdgToplevelClient::isFullScreen() const +{ + return m_isFullScreen; +} + +bool XdgToplevelClient::isMovable() const +{ + if (isFullScreen()) { + return false; } - if (m_xdgDecoration && m_xdgDecoration->requestedMode() != XdgDecorationInterface::Mode::ClientSide) { - return m_userNoBorder || isFullScreen(); + if (isSpecialWindow() && !isSplash() && !isToolbar()) { + return false; + } + if (rules()->checkPosition(invalidPoint) != invalidPoint) { + return false; } return true; } -bool XdgShellClient::isFullScreenable() const +bool XdgToplevelClient::isMovableAcrossScreens() const { - if (!rules()->checkFullScreen(true)) { + if (isSpecialWindow() && !isSplash() && !isToolbar()) { return false; } - return !isSpecialWindow(); + if (rules()->checkPosition(invalidPoint) != invalidPoint) { + return false; + } + return true; } -void XdgShellClient::setFullScreen(bool set, bool user) +bool XdgToplevelClient::isResizable() const { - set = rules()->checkFullScreen(set); - - const bool wasFullscreen = isFullScreen(); - if (wasFullscreen == set) { - return; + if (isFullScreen()) { + return false; } - if (isSpecialWindow()) { - return; + if (isSpecialWindow() || isSplash() || isToolbar()) { + return false; } - if (user && !userCanSetFullScreen()) { - return; + if (rules()->checkSize(QSize()).isValid()) { + return false; } + const QSize min = minSize(); + const QSize max = maxSize(); + return min.width() < max.width() || min.height() < max.height(); +} - if (wasFullscreen) { - workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event - } else { - m_geomFsRestore = frameGeometry(); +bool XdgToplevelClient::isCloseable() const +{ + return !isDesktop() && !isDock(); +} + +bool XdgToplevelClient::isFullScreenable() const +{ + if (!rules()->checkFullScreen(true)) { + return false; } - m_fullScreen = set; + return !isSpecialWindow(); +} - if (set) { - workspace()->raiseClient(this); +bool XdgToplevelClient::isMaximizable() const +{ + if (!isResizable()) { + return false; } - RequestGeometryBlocker requestBlocker(this); - StackingUpdatesBlocker blocker1(workspace()); - GeometryUpdatesBlocker blocker2(this); + if (rules()->checkMaximize(MaximizeRestore) != MaximizeRestore || + rules()->checkMaximize(MaximizeFull) != MaximizeFull) { + return false; + } + return true; +} - workspace()->updateClientLayer(this); // active fullscreens get different layer - updateDecoration(false, false); +bool XdgToplevelClient::isMinimizable() const +{ + if (isSpecialWindow() && !isTransient()) { + return false; + } + if (!rules()->checkMinimize(true)) { + return false; + } + return true; +} - if (set) { - setFrameGeometry(workspace()->clientArea(FullScreenArea, this)); - } else { - if (m_geomFsRestore.isValid()) { - int currentScreen = screen(); - setFrameGeometry(QRect(m_geomFsRestore.topLeft(), constrainFrameSize(m_geomFsRestore.size()))); - if( currentScreen != screen()) - workspace()->sendClientToScreen( this, currentScreen ); - } else { - // this can happen when the window was first shown already fullscreen, - // so let the client set the size by itself - setFrameGeometry(QRect(workspace()->clientArea(PlacementArea, this).topLeft(), QSize(0, 0))); +bool XdgToplevelClient::isTransient() const +{ + return m_isTransient; +} + +bool XdgToplevelClient::userCanSetFullScreen() const +{ + return true; +} + +bool XdgToplevelClient::userCanSetNoBorder() const +{ + if (m_serverDecoration) { + switch (m_serverDecoration->mode()) { + case ServerSideDecorationManagerInterface::Mode::Server: + return !isFullScreen() && !isShade(); + case ServerSideDecorationManagerInterface::Mode::Client: + case ServerSideDecorationManagerInterface::Mode::None: + return false; + } + } + if (m_xdgDecoration) { + switch (m_xdgDecoration->preferredMode()) { + case XdgToplevelDecorationV1Interface::Mode::Server: + case XdgToplevelDecorationV1Interface::Mode::Undefined: + return !isFullScreen() && !isShade(); + case XdgToplevelDecorationV1Interface::Mode::Client: + return false; + } + } + return false; +} + +bool XdgToplevelClient::noBorder() const +{ + if (m_serverDecoration) { + switch (m_serverDecoration->mode()) { + case ServerSideDecorationManagerInterface::Mode::Server: + return m_userNoBorder || isFullScreen(); + case ServerSideDecorationManagerInterface::Mode::Client: + case ServerSideDecorationManagerInterface::Mode::None: + return true; + } + } + if (m_xdgDecoration) { + switch (m_xdgDecoration->preferredMode()) { + case XdgToplevelDecorationV1Interface::Mode::Server: + case XdgToplevelDecorationV1Interface::Mode::Undefined: + return m_userNoBorder || isFullScreen(); + case XdgToplevelDecorationV1Interface::Mode::Client: + return true; } } - - updateWindowRules(Rules::Fullscreen|Rules::Position|Rules::Size); - emit fullScreenChanged(); + return true; } -void XdgShellClient::setNoBorder(bool set) +void XdgToplevelClient::setNoBorder(bool set) { if (!userCanSetNoBorder()) { return; } set = rules()->checkNoBorder(set); if (m_userNoBorder == set) { return; } m_userNoBorder = set; updateDecoration(true, false); updateWindowRules(Rules::NoBorder); } -void XdgShellClient::setOnAllActivities(bool set) -{ - Q_UNUSED(set) -} - -void XdgShellClient::takeFocus() +void XdgToplevelClient::updateDecoration(bool check_workspace_pos, bool force) { - if (rules()->checkAcceptFocus(wantsInput())) { - if (m_xdgShellToplevel) { - ping(PingReason::FocusWindow); + if (!force && ((!isDecorated() && noBorder()) || (isDecorated() && !noBorder()))) { + return; + } + const QRect oldFrameGeometry = frameGeometry(); + const QRect oldClientGeometry = clientGeometry(); + blockGeometryUpdates(true); + if (force) { + destroyDecoration(); + } + if (!noBorder()) { + createDecoration(oldFrameGeometry); + } else { + destroyDecoration(); + } + if (m_serverDecoration && isDecorated()) { + m_serverDecoration->setMode(ServerSideDecorationManagerInterface::Mode::Server); + } + if (m_xdgDecoration) { + if (isDecorated() || m_userNoBorder) { + m_xdgDecoration->sendConfigure(XdgToplevelDecorationV1Interface::Mode::Server); + } else { + m_xdgDecoration->sendConfigure(XdgToplevelDecorationV1Interface::Mode::Client); } - setActive(true); + scheduleConfigure(); } - - if (!keepAbove() && !isOnScreenDisplay() && !belongsToDesktop()) { - workspace()->setShowingDesktop(false); + updateShadow(); + if (check_workspace_pos) { + checkWorkspacePosition(oldFrameGeometry, -2, oldClientGeometry); } + blockGeometryUpdates(false); } -void XdgShellClient::doSetActive() +bool XdgToplevelClient::supportsWindowRules() const { - if (!isActive()) { - return; - } - StackingUpdatesBlocker blocker(workspace()); - workspace()->focusToNull(); + return !m_plasmaShellSurface; } -bool XdgShellClient::userCanSetFullScreen() const +bool XdgToplevelClient::hasStrut() const { - if (m_xdgShellToplevel) { - return true; + if (!isShown(true)) { + return false; } - return false; + if (!m_plasmaShellSurface) { + return false; + } + if (m_plasmaShellSurface->role() != PlasmaShellSurfaceInterface::Role::Panel) { + return false; + } + return m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AlwaysVisible; } -bool XdgShellClient::userCanSetNoBorder() const +void XdgToplevelClient::showOnScreenEdge() { - if (m_serverDecoration && m_serverDecoration->mode() == ServerSideDecorationManagerInterface::Mode::Server) { - return !isFullScreen() && !isShade(); + if (!m_plasmaShellSurface || isUnmapped()) { + return; } - if (m_xdgDecoration && m_xdgDecoration->requestedMode() != XdgDecorationInterface::Mode::ClientSide) { - return !isFullScreen() && !isShade(); + hideClient(false); + workspace()->raiseClient(this); + if (m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AutoHide) { + m_plasmaShellSurface->showAutoHidingPanel(); } - return false; } -bool XdgShellClient::wantsInput() const +bool XdgToplevelClient::isInitialPositionSet() const { - return rules()->checkAcceptFocus(acceptsFocus()); + return m_plasmaShellSurface ? m_plasmaShellSurface->isPositionSet() : false; } -bool XdgShellClient::acceptsFocus() const +void XdgToplevelClient::closeWindow() { - if (waylandServer()->inputMethodConnection() == surface()->client()) { - return false; + if (isCloseable()) { + sendPing(PingReason::CloseWindow); + m_shellSurface->sendClose(); } - if (m_plasmaShellSurface) { - if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::OnScreenDisplay || - m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::ToolTip) { - return false; - } +} - if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Notification || - m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::CriticalNotification) { - return m_plasmaShellSurface->panelTakesFocus(); - } - } - if (m_closing) { - // a closing window does not accept focus - return false; - } - if (m_unmapped) { - // an unmapped window does not accept focus - return false; - } - if (m_xdgShellToplevel) { - // TODO: proper - return true; - } - return false; +XdgSurfaceConfigure *XdgToplevelClient::sendRoleConfigure() const +{ + const quint32 serial = m_shellSurface->sendConfigure(requestedClientSize(), m_requestedStates); + + XdgToplevelConfigure *configureEvent = new XdgToplevelConfigure(); + configureEvent->position = requestedPos(); + configureEvent->size = requestedSize(); + configureEvent->states = m_requestedStates; + configureEvent->serial = serial; + + return configureEvent; } -void XdgShellClient::createWindowId() +bool XdgToplevelClient::stateCompare() const { - m_windowId = waylandServer()->createWindowId(surface()); + if (m_requestedStates != m_acknowledgedStates) { + return true; + } + return XdgSurfaceClient::stateCompare(); } -pid_t XdgShellClient::pid() const +void XdgToplevelClient::handleRoleCommit() { - return surface()->client()->processId(); + auto configureEvent = static_cast(lastAcknowledgedConfigure()); + if (configureEvent) { + handleStatesAcknowledged(configureEvent->states); + } } -bool XdgShellClient::isLockScreen() const +void XdgToplevelClient::doMinimize() { - return surface()->client() == waylandServer()->screenLockerClientConnection(); + if (isMinimized()) { + workspace()->clientHidden(this); + } else { + emit windowShown(this); + } + workspace()->updateMinimizedOfTransients(this); } -bool XdgShellClient::isInputMethod() const +void XdgToplevelClient::doResizeSync() { - return surface()->client() == waylandServer()->inputMethodConnection(); + requestGeometry(moveResizeGeometry()); } -void XdgShellClient::requestGeometry(const QRect &rect) +void XdgToplevelClient::doSetActive() { - if (m_requestGeometryBlockCounter != 0) { - m_blockedRequestGeometry = rect; - return; + WaylandClient::doSetActive(); + + if (isActive()) { + m_requestedStates |= XdgToplevelInterface::State::Activated; + } else { + m_requestedStates &= ~XdgToplevelInterface::State::Activated; } - QSize size; - if (rect.isValid()) { - size = rect.size() - QSize(borderLeft() + borderRight(), borderTop() + borderBottom()); + scheduleConfigure(); +} + +void XdgToplevelClient::doSetFullScreen() +{ + if (isFullScreen()) { + m_requestedStates |= XdgToplevelInterface::State::FullScreen; } else { - size = QSize(0, 0); + m_requestedStates &= ~XdgToplevelInterface::State::FullScreen; } - m_requestedClientSize = size; - quint64 serialId = 0; + scheduleConfigure(); +} - if (m_xdgShellToplevel) { - serialId = m_xdgShellToplevel->configure(xdgSurfaceStates(), size); +void XdgToplevelClient::doSetMaximized() +{ + if (requestedMaximizeMode() & MaximizeHorizontal) { + m_requestedStates |= XdgToplevelInterface::State::MaximizedHorizontal; + } else { + m_requestedStates &= ~XdgToplevelInterface::State::MaximizedHorizontal; } - if (m_xdgShellPopup) { - auto parent = transientFor(); - if (parent) { - const QPoint globalClientContentPos = parent->frameGeometry().topLeft() + parent->clientPos(); - const QPoint relativeOffset = rect.topLeft() - globalClientContentPos; - serialId = m_xdgShellPopup->configure(QRect(relativeOffset, size)); - } + + if (requestedMaximizeMode() & MaximizeVertical) { + m_requestedStates |= XdgToplevelInterface::State::MaximizedVertical; + } else { + m_requestedStates &= ~XdgToplevelInterface::State::MaximizedVertical; } - if (rect.isValid()) { //if there's no requested size, then there's implicity no positional information worth using - PendingConfigureRequest configureRequest; - configureRequest.serialId = serialId; - configureRequest.positionAfterResize = rect.topLeft(); - configureRequest.maximizeMode = m_requestedMaximizeMode; - m_pendingConfigureRequests.append(configureRequest); + scheduleConfigure(); +} + +bool XdgToplevelClient::doStartMoveResize() +{ + if (moveResizePointerMode() != PositionCenter) { + m_requestedStates |= XdgToplevelInterface::State::Resizing; } - m_blockedRequestGeometry = QRect(); + scheduleConfigure(); + return true; } -void XdgShellClient::updatePendingGeometry() +void XdgToplevelClient::doFinishMoveResize() { - QPoint position = pos(); - MaximizeMode maximizeMode = m_maximizeMode; - for (auto it = m_pendingConfigureRequests.begin(); it != m_pendingConfigureRequests.end(); it++) { - if (it->serialId > m_lastAckedConfigureRequest) { - //this serial is not acked yet, therefore we know all future serials are not - break; - } - if (it->serialId == m_lastAckedConfigureRequest) { - if (position != it->positionAfterResize) { - addLayerRepaint(frameGeometry()); - } - position = it->positionAfterResize; - maximizeMode = it->maximizeMode; + m_requestedStates &= ~XdgToplevelInterface::State::Resizing; + scheduleConfigure(); +} - m_pendingConfigureRequests.erase(m_pendingConfigureRequests.begin(), ++it); - break; - } - //else serialId < m_lastAckedConfigureRequest and the state is now irrelevant and can be ignored - } - QRect geometry = QRect(position, adjustedSize()); - if (isMove()) { - geometry = adjustMoveGeometry(geometry); +void XdgToplevelClient::takeFocus() +{ + if (wantsInput()) { + sendPing(PingReason::FocusWindow); + setActive(true); } - if (isResize()) { - geometry = adjustResizeGeometry(geometry); + if (!keepAbove() && !isOnScreenDisplay() && !belongsToDesktop()) { + workspace()->setShowingDesktop(false); } - doSetGeometry(geometry); - updateMaximizeMode(maximizeMode); } -void XdgShellClient::handleConfigureAcknowledged(quint32 serial) +bool XdgToplevelClient::wantsInput() const { - m_lastAckedConfigureRequest = serial; + return rules()->checkAcceptFocus(acceptsFocus()); } -void XdgShellClient::handleTransientForChanged() +bool XdgToplevelClient::dockWantsInput() const { - SurfaceInterface *transientSurface = nullptr; - if (m_xdgShellToplevel) { - if (auto transient = m_xdgShellToplevel->transientFor().data()) { - transientSurface = transient->surface(); + if (m_plasmaShellSurface) { + if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Panel) { + return m_plasmaShellSurface->panelTakesFocus(); } } - if (m_xdgShellPopup) { - transientSurface = m_xdgShellPopup->transientFor().data(); - } - if (!transientSurface) { - transientSurface = waylandServer()->findForeignTransientForSurface(surface()); + return false; +} + +bool XdgToplevelClient::acceptsFocus() const +{ + if (isInputMethod()) { + return false; } - AbstractClient *transientClient = waylandServer()->findClient(transientSurface); - if (transientClient != transientFor()) { - // Remove from main client. - if (transientFor()) { - transientFor()->removeTransient(this); + if (m_plasmaShellSurface) { + if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::OnScreenDisplay || + m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::ToolTip) { + return false; } - setTransientFor(transientClient); - if (transientClient) { - transientClient->addTransient(this); + if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Notification || + m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::CriticalNotification) { + return m_plasmaShellSurface->panelTakesFocus(); } } - m_transient = (transientSurface != nullptr); + return !isClosing() && !isUnmapped(); } -void XdgShellClient::handleWindowClassChanged(const QByteArray &windowClass) +Layer XdgToplevelClient::layerForDock() const { - setResourceClass(resourceName(), windowClass); - if (m_isInitialized && supportsWindowRules()) { - setupWindowRules(true); - applyWindowRules(); + if (m_plasmaShellSurface) { + switch (m_plasmaShellSurface->panelBehavior()) { + case PlasmaShellSurfaceInterface::PanelBehavior::WindowsCanCover: + return NormalLayer; + case PlasmaShellSurfaceInterface::PanelBehavior::AutoHide: + return AboveLayer; + case PlasmaShellSurfaceInterface::PanelBehavior::WindowsGoBelow: + case PlasmaShellSurfaceInterface::PanelBehavior::AlwaysVisible: + return DockLayer; + default: + Q_UNREACHABLE(); + break; + } } - setDesktopFileName(windowClass); + return AbstractClient::layerForDock(); } -void XdgShellClient::handleWindowGeometryChanged(const QRect &windowGeometry) +void XdgToplevelClient::handleWindowTitleChanged() { - m_windowGeometry = windowGeometry; - m_hasWindowGeometry = true; + setCaption(m_shellSurface->windowTitle()); } -void XdgShellClient::handleWindowTitleChanged(const QString &title) +void XdgToplevelClient::handleWindowClassChanged() { - const QString oldSuffix = m_captionSuffix; - m_caption = title.simplified(); - updateCaption(); - if (m_captionSuffix == oldSuffix) { - // Don't emit caption change twice it already got emitted by the changing suffix. - emit captionChanged(); + const QByteArray applicationId = m_shellSurface->windowClass().toUtf8(); + setResourceClass(resourceName(), applicationId); + if (shellSurface()->isConfigured() && supportsWindowRules()) { + evaluateWindowRules(); } + setDesktopFileName(applicationId); +} + +void XdgToplevelClient::handleWindowMenuRequested(SeatInterface *seat, const QPoint &surfacePos, + quint32 serial) +{ + Q_UNUSED(seat) + Q_UNUSED(serial) + performMouseCommand(Options::MouseOperationsMenu, pos() + surfacePos); } -void XdgShellClient::handleMoveRequested(SeatInterface *seat, quint32 serial) +void XdgToplevelClient::handleMoveRequested(SeatInterface *seat, quint32 serial) { - // FIXME: Check the seat and serial. Q_UNUSED(seat) Q_UNUSED(serial) performMouseCommand(Options::MouseMove, Cursors::self()->mouse()->pos()); } -void XdgShellClient::handleResizeRequested(SeatInterface *seat, quint32 serial, Qt::Edges edges) +void XdgToplevelClient::handleResizeRequested(SeatInterface *seat, Qt::Edges edges, quint32 serial) { - // FIXME: Check the seat and serial. Q_UNUSED(seat) Q_UNUSED(serial) if (!isResizable() || isShade()) { return; } if (isMoveResize()) { finishMoveResize(false); } setMoveResizePointerButtonDown(true); setMoveOffset(Cursors::self()->mouse()->pos() - pos()); // map from global setInvertedMoveOffset(rect().bottomRight() - moveOffset()); setUnrestrictedMoveResize(false); auto toPosition = [edges] { Position position = PositionCenter; if (edges.testFlag(Qt::TopEdge)) { position = PositionTop; } else if (edges.testFlag(Qt::BottomEdge)) { position = PositionBottom; } if (edges.testFlag(Qt::LeftEdge)) { position = Position(position | PositionLeft); } else if (edges.testFlag(Qt::RightEdge)) { position = Position(position | PositionRight); } return position; }; setMoveResizePointerMode(toPosition()); if (!startMoveResize()) { setMoveResizePointerButtonDown(false); } updateCursor(); } -void XdgShellClient::handleMinimizeRequested() +void XdgToplevelClient::handleStatesAcknowledged(const XdgToplevelInterface::States &states) +{ + const XdgToplevelInterface::States delta = m_acknowledgedStates ^ states; + + if (delta & XdgToplevelInterface::State::Maximized) { + MaximizeMode maximizeMode = MaximizeRestore; + if (states & XdgToplevelInterface::State::MaximizedHorizontal) { + maximizeMode = MaximizeMode(maximizeMode | MaximizeHorizontal); + } + if (states & XdgToplevelInterface::State::MaximizedVertical) { + maximizeMode = MaximizeMode(maximizeMode | MaximizeVertical); + } + updateMaximizeMode(maximizeMode); + } + if (delta & XdgToplevelInterface::State::FullScreen) { + updateFullScreenMode(states & XdgToplevelInterface::State::FullScreen); + } + + m_acknowledgedStates = states; +} + +void XdgToplevelClient::handleMaximizeRequested() +{ + maximize(MaximizeFull); + scheduleConfigure(); +} + +void XdgToplevelClient::handleUnmaximizeRequested() +{ + maximize(MaximizeRestore); + scheduleConfigure(); +} + +void XdgToplevelClient::handleFullscreenRequested(OutputInterface *output) +{ + Q_UNUSED(output) + setFullScreen(/* set */ true, /* user */ false); + scheduleConfigure(); +} + +void XdgToplevelClient::handleUnfullscreenRequested() +{ + setFullScreen(/* set */ false, /* user */ false); + scheduleConfigure(); +} + +void XdgToplevelClient::handleMinimizeRequested() { performMouseCommand(Options::MouseMinimize, Cursors::self()->mouse()->pos()); } -void XdgShellClient::handleMaximizeRequested(bool maximized) +void XdgToplevelClient::handleTransientForChanged() +{ + SurfaceInterface *transientForSurface = nullptr; + if (XdgToplevelInterface *parentToplevel = m_shellSurface->parentXdgToplevel()) { + transientForSurface = parentToplevel->surface(); + } + if (!transientForSurface) { + transientForSurface = waylandServer()->findForeignTransientForSurface(surface()); + } + AbstractClient *transientForClient = waylandServer()->findClient(transientForSurface); + if (transientForClient != transientFor()) { + if (transientFor()) { + transientFor()->removeTransient(this); + } + if (transientForClient) { + transientForClient->addTransient(this); + } + setTransientFor(transientForClient); + } + m_isTransient = transientForClient; +} + +void XdgToplevelClient::handleForeignTransientForChanged(SurfaceInterface *child) +{ + if (surface() == child) { + handleTransientForChanged(); + } +} + +void XdgToplevelClient::handlePingTimeout(quint32 serial) +{ + auto pingIt = m_pings.find(serial); + if (pingIt == m_pings.end()) { + return; + } + if (pingIt.value() == PingReason::CloseWindow) { + qCDebug(KWIN_CORE) << "Final ping timeout on a close attempt, asking to kill:" << caption(); + + //for internal windows, killing the window will delete this + QPointer guard(this); + killWindow(); + if (!guard) { + return; + } + } + m_pings.erase(pingIt); +} + +void XdgToplevelClient::handlePingDelayed(quint32 serial) +{ + auto it = m_pings.find(serial); + if (it != m_pings.end()) { + qCDebug(KWIN_CORE) << "First ping timeout:" << caption(); + setUnresponsive(true); + } +} + +void XdgToplevelClient::handlePongReceived(quint32 serial) +{ + auto it = m_pings.find(serial); + if (it != m_pings.end()) { + setUnresponsive(false); + m_pings.erase(it); + } +} + +void XdgToplevelClient::sendPing(PingReason reason) +{ + XdgShellInterface *shell = m_shellSurface->shell(); + XdgSurfaceInterface *surface = m_shellSurface->xdgSurface(); + + const quint32 serial = shell->ping(surface); + m_pings.insert(serial, reason); +} + +void XdgToplevelClient::initialize() { - // If the maximized state of the client hasn't been changed due to a window - // rule or because the requested state is the same as the current, then the - // compositor still has to send a configure event. - RequestGeometryBlocker blocker(this); + blockGeometryUpdates(true); + + bool needsPlacement = !isInitialPositionSet(); + + if (supportsWindowRules()) { + setupWindowRules(false); + + const QRect originalGeometry = frameGeometry(); + const QRect ruledGeometry = rules()->checkGeometry(originalGeometry, true); + if (originalGeometry != ruledGeometry) { + setFrameGeometry(ruledGeometry); + } + maximize(rules()->checkMaximize(maximizeMode(), true)); + setDesktop(rules()->checkDesktop(desktop(), true)); + setDesktopFileName(rules()->checkDesktopFile(desktopFileName(), true).toUtf8()); + if (rules()->checkMinimize(isMinimized(), true)) { + minimize(true); // No animation. + } + setSkipTaskbar(rules()->checkSkipTaskbar(skipTaskbar(), true)); + setSkipPager(rules()->checkSkipPager(skipPager(), true)); + setSkipSwitcher(rules()->checkSkipSwitcher(skipSwitcher(), true)); + setKeepAbove(rules()->checkKeepAbove(keepAbove(), true)); + setKeepBelow(rules()->checkKeepBelow(keepBelow(), true)); + setShortcut(rules()->checkShortcut(shortcut().toString(), true)); + + // Don't place the client if its position is set by a rule. + if (rules()->checkPosition(invalidPoint, true) != invalidPoint) { + needsPlacement = false; + } + + // Don't place the client if the maximize state is set by a rule. + if (requestedMaximizeMode() != MaximizeRestore) { + needsPlacement = false; + } + + discardTemporaryRules(); + RuleBook::self()->discardUsed(this, false); // Remove Apply Now rules. + updateWindowRules(Rules::All); + } + if (isFullScreen()) { + needsPlacement = false; + } + if (needsPlacement) { + const QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop()); + placeIn(area); + } - maximize(maximized ? MaximizeFull : MaximizeRestore); + blockGeometryUpdates(false); + scheduleConfigure(); + updateColorScheme(); } -void XdgShellClient::handleFullScreenRequested(bool fullScreen, OutputInterface *output) +void XdgToplevelClient::updateMaximizeMode(MaximizeMode maximizeMode) { - // FIXME: Consider output as well. - Q_UNUSED(output); - setFullScreen(fullScreen, false); + if (m_maximizeMode == maximizeMode) { + return; + } + m_maximizeMode = maximizeMode; + updateWindowRules(Rules::MaximizeVert | Rules::MaximizeHoriz); + emit clientMaximizedStateChanged(this, maximizeMode); + emit clientMaximizedStateChanged(this, maximizeMode & MaximizeHorizontal, maximizeMode & MaximizeVertical); } -void XdgShellClient::handleWindowMenuRequested(SeatInterface *seat, quint32 serial, const QPoint &surfacePos) +void XdgToplevelClient::updateFullScreenMode(bool set) { - // FIXME: Check the seat and serial. - Q_UNUSED(seat) - Q_UNUSED(serial) - performMouseCommand(Options::MouseOperationsMenu, pos() + surfacePos); + if (m_isFullScreen == set) { + return; + } + m_isFullScreen = set; + updateWindowRules(Rules::Fullscreen); + emit fullScreenChanged(); } -void XdgShellClient::handleGrabRequested(SeatInterface *seat, quint32 serial) +void XdgToplevelClient::updateColorScheme() { - // FIXME: Check the seat and serial as well whether the parent had focus. - Q_UNUSED(seat) - Q_UNUSED(serial) - m_hasPopupGrab = true; + if (m_paletteInterface) { + AbstractClient::updateColorScheme(rules()->checkDecoColor(m_paletteInterface->palette())); + } else { + AbstractClient::updateColorScheme(rules()->checkDecoColor(QString())); + } } -void XdgShellClient::handlePingDelayed(quint32 serial) +void XdgToplevelClient::installAppMenu(AppMenuInterface *appMenu) { - auto it = m_pingSerials.find(serial); - if (it != m_pingSerials.end()) { - qCDebug(KWIN_CORE) << "First ping timeout:" << caption(); - setUnresponsive(true); - } + m_appMenuInterface = appMenu; + + auto updateMenu = [this](const AppMenuInterface::InterfaceAddress &address) { + updateApplicationMenuServiceName(address.serviceName); + updateApplicationMenuObjectPath(address.objectPath); + }; + connect(m_appMenuInterface, &AppMenuInterface::addressChanged, this, updateMenu); + updateMenu(appMenu->address()); } -void XdgShellClient::handlePingTimeout(quint32 serial) +void XdgToplevelClient::installServerDecoration(ServerSideDecorationInterface *decoration) { - auto it = m_pingSerials.find(serial); - if (it != m_pingSerials.end()) { - if (it.value() == PingReason::CloseWindow) { - qCDebug(KWIN_CORE) << "Final ping timeout on a close attempt, asking to kill:" << caption(); + m_serverDecoration = decoration; - //for internal windows, killing the window will delete this - QPointer guard(this); - killWindow(); - if (!guard) { - return; + connect(m_serverDecoration, &ServerSideDecorationInterface::destroyed, this, [this] { + if (!isClosing() && !isUnmapped()) { + updateDecoration(/* check_workspace_pos */ true); + } + }); + connect(m_serverDecoration, &ServerSideDecorationInterface::modeRequested, this, + [this] (ServerSideDecorationManagerInterface::Mode mode) { + const bool changed = mode != m_serverDecoration->mode(); + if (changed && !isUnmapped()) { + updateDecoration(/* check_workspace_pos */ false); } } - m_pingSerials.erase(it); - } -} - -void XdgShellClient::handlePongReceived(quint32 serial) -{ - auto it = m_pingSerials.find(serial); - if (it != m_pingSerials.end()) { - setUnresponsive(false); - m_pingSerials.erase(it); + ); + if (!isUnmapped()) { + updateDecoration(/* check_workspace_pos */ true); } } -void XdgShellClient::handleCommitted() +void XdgToplevelClient::installXdgDecoration(XdgToplevelDecorationV1Interface *decoration) { - if (!surface()->buffer()) { - return; - } - - if (!m_hasWindowGeometry) { - m_windowGeometry = surface()->boundingRect(); - } + m_xdgDecoration = decoration; - updatePendingGeometry(); - - setDepth((surface()->buffer()->hasAlphaChannel() && !isDesktop()) ? 32 : 24); - markAsMapped(); + connect(m_xdgDecoration, &XdgToplevelDecorationV1Interface::destroyed, this, [this] { + if (!isClosing()) { + updateDecoration(/* check_workspace_pos */ true); + } + }); + connect(m_xdgDecoration, &XdgToplevelDecorationV1Interface::preferredModeChanged, this, [this] { + // force is true as we must send a new configure response. + updateDecoration(/* check_workspace_pos */ false, /* force */ true); + }); } -void XdgShellClient::resizeWithChecks(const QSize &size, ForceGeometry_t force) +void XdgToplevelClient::installPalette(ServerSideDecorationPaletteInterface *palette) { - // don't allow growing larger than workarea - const QRect area = workspace()->clientArea(WorkArea, this); - setFrameGeometry(QRect{pos(), size.boundedTo(area.size())}, force); -} + m_paletteInterface = palette; -void XdgShellClient::unmap() -{ - m_unmapped = true; - if (isMoveResize()) { - leaveMoveResize(); - } - m_requestedClientSize = QSize(0, 0); - destroyWindowManagementInterface(); - if (Workspace::self()) { - addWorkspaceRepaint(visibleRect()); - workspace()->clientHidden(this); - } - emit windowHidden(this); + auto updatePalette = [this](const QString &palette) { + AbstractClient::updateColorScheme(rules()->checkDecoColor(palette)); + }; + connect(m_paletteInterface, &ServerSideDecorationPaletteInterface::paletteChanged, this, [=](const QString &palette) { + updatePalette(palette); + }); + connect(m_paletteInterface, &QObject::destroyed, this, [=]() { + updatePalette(QString()); + }); + updatePalette(palette->palette()); } -void XdgShellClient::installPlasmaShellSurface(PlasmaShellSurfaceInterface *surface) +/** + * \todo This whole plasma shell surface thing doesn't seem right. It turns xdg-toplevel into + * something completely different! Perhaps plasmashell surfaces need to be implemented via a + * proprietary protocol that doesn't piggyback on existing shell surface protocols. It'll lead + * to cleaner code and will be technically correct, but I'm not sure whether this is do-able. + */ +void XdgToplevelClient::installPlasmaShellSurface(PlasmaShellSurfaceInterface *shellSurface) { - m_plasmaShellSurface = surface; - auto updatePosition = [this, surface] { - // That's a mis-use of doSetGeometry method. One should instead use move method. - QRect rect = QRect(surface->position(), size()); - doSetGeometry(rect); - }; - auto updateRole = [this, surface] { + m_plasmaShellSurface = shellSurface; + + auto updatePosition = [this, shellSurface] { move(shellSurface->position()); }; + auto updateRole = [this, shellSurface] { NET::WindowType type = NET::Unknown; - switch (surface->role()) { + switch (shellSurface->role()) { case PlasmaShellSurfaceInterface::Role::Desktop: type = NET::Desktop; break; case PlasmaShellSurfaceInterface::Role::Panel: type = NET::Dock; break; case PlasmaShellSurfaceInterface::Role::OnScreenDisplay: type = NET::OnScreenDisplay; break; case PlasmaShellSurfaceInterface::Role::Notification: type = NET::Notification; break; case PlasmaShellSurfaceInterface::Role::ToolTip: type = NET::Tooltip; break; case PlasmaShellSurfaceInterface::Role::CriticalNotification: type = NET::CriticalNotification; break; case PlasmaShellSurfaceInterface::Role::Normal: default: type = NET::Normal; break; } - if (type != m_windowType) { - m_windowType = type; - if (m_windowType == NET::Desktop || type == NET::Dock || type == NET::OnScreenDisplay || type == NET::Notification || type == NET::Tooltip || type == NET::CriticalNotification) { - setOnAllDesktops(true); - } - workspace()->updateClientArea(); + if (m_windowType == type) { + return; + } + m_windowType = type; + switch (m_windowType) { + case NET::Desktop: + case NET::Dock: + case NET::OnScreenDisplay: + case NET::Notification: + case NET::CriticalNotification: + case NET::Tooltip: + setOnAllDesktops(true); + break; + default: + break; } + workspace()->updateClientArea(); }; - connect(surface, &PlasmaShellSurfaceInterface::panelTakesFocusChanged , this, [this, surface]() { - if (surface->panelTakesFocus()) { + connect(shellSurface, &PlasmaShellSurfaceInterface::positionChanged, this, updatePosition); + connect(shellSurface, &PlasmaShellSurfaceInterface::roleChanged, this, updateRole); + connect(shellSurface, &PlasmaShellSurfaceInterface::panelBehaviorChanged, this, [this] { + updateShowOnScreenEdge(); + workspace()->updateClientArea(); + }); + connect(shellSurface, &PlasmaShellSurfaceInterface::panelAutoHideHideRequested, this, [this] { + hideClient(true); + m_plasmaShellSurface->hideAutoHidingPanel(); + updateShowOnScreenEdge(); + }); + connect(shellSurface, &PlasmaShellSurfaceInterface::panelAutoHideShowRequested, this, [this] { + hideClient(false); + ScreenEdges::self()->reserve(this, ElectricNone); + m_plasmaShellSurface->showAutoHidingPanel(); + }); + connect(shellSurface, &PlasmaShellSurfaceInterface::panelTakesFocusChanged, this, [this] { + if (m_plasmaShellSurface->panelTakesFocus()) { workspace()->activateClient(this); } }); - connect(surface, &PlasmaShellSurfaceInterface::positionChanged, this, updatePosition); - connect(surface, &PlasmaShellSurfaceInterface::roleChanged, this, updateRole); - connect(surface, &PlasmaShellSurfaceInterface::panelBehaviorChanged, this, - [this] { - updateShowOnScreenEdge(); - workspace()->updateClientArea(); - } - ); - connect(surface, &PlasmaShellSurfaceInterface::panelAutoHideHideRequested, this, - [this] { - hideClient(true); - m_plasmaShellSurface->hideAutoHidingPanel(); - updateShowOnScreenEdge(); - } - ); - connect(surface, &PlasmaShellSurfaceInterface::panelAutoHideShowRequested, this, - [this] { - hideClient(false); - ScreenEdges::self()->reserve(this, ElectricNone); - m_plasmaShellSurface->showAutoHidingPanel(); - } - ); - if (surface->isPositionSet()) + if (shellSurface->isPositionSet()) { updatePosition(); + } updateRole(); updateShowOnScreenEdge(); - connect(this, &XdgShellClient::frameGeometryChanged, this, &XdgShellClient::updateShowOnScreenEdge); + connect(this, &XdgToplevelClient::frameGeometryChanged, + this, &XdgToplevelClient::updateShowOnScreenEdge); + connect(this, &XdgToplevelClient::windowShown, + this, &XdgToplevelClient::updateShowOnScreenEdge); - setSkipTaskbar(surface->skipTaskbar()); - connect(surface, &PlasmaShellSurfaceInterface::skipTaskbarChanged, this, [this] { + setSkipTaskbar(shellSurface->skipTaskbar()); + connect(shellSurface, &PlasmaShellSurfaceInterface::skipTaskbarChanged, this, [this] { setSkipTaskbar(m_plasmaShellSurface->skipTaskbar()); }); - setSkipSwitcher(surface->skipSwitcher()); - connect(surface, &PlasmaShellSurfaceInterface::skipSwitcherChanged, this, [this] { + setSkipSwitcher(shellSurface->skipSwitcher()); + connect(shellSurface, &PlasmaShellSurfaceInterface::skipSwitcherChanged, this, [this] { setSkipSwitcher(m_plasmaShellSurface->skipSwitcher()); }); } -void XdgShellClient::updateShowOnScreenEdge() +void XdgToplevelClient::updateShowOnScreenEdge() { if (!ScreenEdges::self()) { return; } - if (m_unmapped || !m_plasmaShellSurface || m_plasmaShellSurface->role() != PlasmaShellSurfaceInterface::Role::Panel) { + if (isUnmapped() || !m_plasmaShellSurface || + m_plasmaShellSurface->role() != PlasmaShellSurfaceInterface::Role::Panel) { ScreenEdges::self()->reserve(this, ElectricNone); return; } - if ((m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AutoHide && m_hidden) || - m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::WindowsCanCover) { - // screen edge API requires an edge, thus we need to figure out which edge the window borders + const PlasmaShellSurfaceInterface::PanelBehavior panelBehavior = m_plasmaShellSurface->panelBehavior(); + if ((panelBehavior == PlasmaShellSurfaceInterface::PanelBehavior::AutoHide && isHidden()) || + panelBehavior == PlasmaShellSurfaceInterface::PanelBehavior::WindowsCanCover) { + // Screen edge API requires an edge, thus we need to figure out which edge the window borders. const QRect clientGeometry = frameGeometry(); Qt::Edges edges; for (int i = 0; i < screens()->count(); i++) { const QRect screenGeometry = screens()->geometry(i); if (screenGeometry.left() == clientGeometry.left()) { edges |= Qt::LeftEdge; } if (screenGeometry.right() == clientGeometry.right()) { edges |= Qt::RightEdge; } if (screenGeometry.top() == clientGeometry.top()) { edges |= Qt::TopEdge; } if (screenGeometry.bottom() == clientGeometry.bottom()) { edges |= Qt::BottomEdge; } } - // a panel might border multiple screen edges. E.g. a horizontal panel at the bottom will - // also border the left and right edge - // let's remove such cases - if (edges.testFlag(Qt::LeftEdge) && edges.testFlag(Qt::RightEdge)) { + + // A panel might border multiple screen edges. E.g. a horizontal panel at the bottom will + // also border the left and right edge. Let's remove such cases. + if (edges & Qt::LeftEdge && edges & Qt::RightEdge) { edges = edges & (~(Qt::LeftEdge | Qt::RightEdge)); } - if (edges.testFlag(Qt::TopEdge) && edges.testFlag(Qt::BottomEdge)) { + if (edges & Qt::TopEdge && edges & Qt::BottomEdge) { edges = edges & (~(Qt::TopEdge | Qt::BottomEdge)); } - // it's still possible that a panel borders two edges, e.g. bottom and left - // in that case the one which is sharing more with the edge wins - auto check = [clientGeometry](Qt::Edges edges, Qt::Edge horiz, Qt::Edge vert) { - if (edges.testFlag(horiz) && edges.testFlag(vert)) { + + // It's still possible that a panel borders two edges, e.g. bottom and left + // in that case the one which is sharing more with the edge wins. + auto check = [clientGeometry](Qt::Edges edges, Qt::Edge horizontal, Qt::Edge vertical) { + if (edges & horizontal && edges & vertical) { if (clientGeometry.width() >= clientGeometry.height()) { - return edges & ~horiz; + return edges & ~horizontal; } else { - return edges & ~vert; + return edges & ~vertical; } } return edges; }; edges = check(edges, Qt::LeftEdge, Qt::TopEdge); edges = check(edges, Qt::LeftEdge, Qt::BottomEdge); edges = check(edges, Qt::RightEdge, Qt::TopEdge); edges = check(edges, Qt::RightEdge, Qt::BottomEdge); ElectricBorder border = ElectricNone; - if (edges.testFlag(Qt::LeftEdge)) { + if (edges & Qt::LeftEdge) { border = ElectricLeft; } - if (edges.testFlag(Qt::RightEdge)) { + if (edges & Qt::RightEdge) { border = ElectricRight; } - if (edges.testFlag(Qt::TopEdge)) { + if (edges & Qt::TopEdge) { border = ElectricTop; } - if (edges.testFlag(Qt::BottomEdge)) { + if (edges & Qt::BottomEdge) { border = ElectricBottom; } ScreenEdges::self()->reserve(this, border); } else { ScreenEdges::self()->reserve(this, ElectricNone); } } -bool XdgShellClient::isInitialPositionSet() const +void XdgToplevelClient::setupWindowManagementIntegration() { - if (m_plasmaShellSurface) { - return m_plasmaShellSurface->isPositionSet(); + if (isLockScreen()) { + return; } - return false; + connect(this, &XdgToplevelClient::windowMapped, + this, &XdgToplevelClient::setupWindowManagementInterface); + connect(this, &XdgToplevelClient::windowUnmapped, + this, &XdgToplevelClient::destroyWindowManagementInterface); } -void XdgShellClient::installAppMenu(AppMenuInterface *menu) +void XdgToplevelClient::setupPlasmaShellIntegration() { - m_appMenuInterface = menu; + connect(this, &XdgToplevelClient::windowMapped, + this, &XdgToplevelClient::updateShowOnScreenEdge); +} - auto updateMenu = [this](AppMenuInterface::InterfaceAddress address) { - updateApplicationMenuServiceName(address.serviceName); - updateApplicationMenuObjectPath(address.objectPath); - }; - connect(m_appMenuInterface, &AppMenuInterface::addressChanged, this, [=](AppMenuInterface::InterfaceAddress address) { - updateMenu(address); - }); - updateMenu(menu->address()); +void XdgToplevelClient::setFullScreen(bool set, bool user) +{ + set = rules()->checkFullScreen(set); + + const bool wasFullscreen = isFullScreen(); + if (wasFullscreen == set) { + return; + } + if (isSpecialWindow()) { + return; + } + if (user && !userCanSetFullScreen()) { + return; + } + + if (wasFullscreen) { + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event + } else { + m_fullScreenGeometryRestore = frameGeometry(); + } + m_isFullScreen = set; + + if (set) { + workspace()->raiseClient(this); + } + StackingUpdatesBlocker blocker1(workspace()); + GeometryUpdatesBlocker blocker2(this); + + workspace()->updateClientLayer(this); // active fullscreens get different layer + updateDecoration(false, false); + + if (set) { + setFrameGeometry(workspace()->clientArea(FullScreenArea, this)); + } else { + if (m_fullScreenGeometryRestore.isValid()) { + int currentScreen = screen(); + setFrameGeometry(QRect(m_fullScreenGeometryRestore.topLeft(), + constrainFrameSize(m_fullScreenGeometryRestore.size()))); + if( currentScreen != screen()) + workspace()->sendClientToScreen( this, currentScreen ); + } else { + // this can happen when the window was first shown already fullscreen, + // so let the client set the size by itself + setFrameGeometry(QRect(workspace()->clientArea(PlacementArea, this).topLeft(), QSize(0, 0))); + } + } + + doSetFullScreen(); + + updateWindowRules(Rules::Fullscreen|Rules::Position|Rules::Size); + emit fullScreenChanged(); } -void XdgShellClient::installPalette(ServerSideDecorationPaletteInterface *palette) +/** + * \todo Move to AbstractClient. + */ +static bool changeMaximizeRecursion = false; +void XdgToplevelClient::changeMaximize(bool horizontal, bool vertical, bool adjust) { - m_paletteInterface = palette; + if (changeMaximizeRecursion) { + return; + } - auto updatePalette = [this](const QString &palette) { - AbstractClient::updateColorScheme(rules()->checkDecoColor(palette)); - }; - connect(m_paletteInterface, &ServerSideDecorationPaletteInterface::paletteChanged, this, [=](const QString &palette) { - updatePalette(palette); - }); - connect(m_paletteInterface, &QObject::destroyed, this, [=]() { - updatePalette(QString()); - }); - updatePalette(palette->palette()); + if (!isResizable()) { + return; + } + + const QRect clientArea = isElectricBorderMaximizing() ? + workspace()->clientArea(MaximizeArea, Cursors::self()->mouse()->pos(), desktop()) : + workspace()->clientArea(MaximizeArea, this); + + const MaximizeMode oldMode = m_requestedMaximizeMode; + const QRect oldGeometry = frameGeometry(); + + // 'adjust == true' means to update the size only, e.g. after changing workspace size + if (!adjust) { + if (vertical) + m_requestedMaximizeMode = MaximizeMode(m_requestedMaximizeMode ^ MaximizeVertical); + if (horizontal) + m_requestedMaximizeMode = MaximizeMode(m_requestedMaximizeMode ^ MaximizeHorizontal); + } + + m_requestedMaximizeMode = rules()->checkMaximize(m_requestedMaximizeMode); + if (!adjust && m_requestedMaximizeMode == oldMode) { + return; + } + + StackingUpdatesBlocker blocker(workspace()); + + // call into decoration update borders + if (isDecorated() && decoration()->client() && !(options->borderlessMaximizedWindows() && m_requestedMaximizeMode == KWin::MaximizeFull)) { + changeMaximizeRecursion = true; + const auto c = decoration()->client().toStrongRef(); + if ((m_requestedMaximizeMode & MaximizeVertical) != (oldMode & MaximizeVertical)) { + emit c->maximizedVerticallyChanged(m_requestedMaximizeMode & MaximizeVertical); + } + if ((m_requestedMaximizeMode & MaximizeHorizontal) != (oldMode & MaximizeHorizontal)) { + emit c->maximizedHorizontallyChanged(m_requestedMaximizeMode & MaximizeHorizontal); + } + if ((m_requestedMaximizeMode == MaximizeFull) != (oldMode == MaximizeFull)) { + emit c->maximizedChanged(m_requestedMaximizeMode & 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(m_requestedMaximizeMode == MaximizeFull)); + changeMaximizeRecursion = false; + } + + // Conditional quick tiling exit points + const auto oldQuickTileMode = quickTileMode(); + if (quickTileMode() != QuickTileMode(QuickTileFlag::None)) { + if (oldMode == MaximizeFull && + !clientArea.contains(geometryRestore().center())) { + // Not restoring on the same screen + // TODO: The following doesn't work for some reason + //quick_tile_mode = QuickTileNone; // And exit quick tile mode manually + } else if ((oldMode == MaximizeVertical && m_requestedMaximizeMode == MaximizeRestore) || + (oldMode == MaximizeFull && m_requestedMaximizeMode == MaximizeHorizontal)) { + // Modifying geometry of a tiled window + updateQuickTileMode(QuickTileFlag::None); // Exit quick tile mode without restoring geometry + } + } + + if (m_requestedMaximizeMode == MaximizeFull) { + setGeometryRestore(oldGeometry); + // TODO: Client has more checks + if (options->electricBorderMaximize()) { + updateQuickTileMode(QuickTileFlag::Maximize); + } else { + updateQuickTileMode(QuickTileFlag::None); + } + if (quickTileMode() != oldQuickTileMode) { + emit quickTileModeChanged(); + } + setFrameGeometry(workspace()->clientArea(MaximizeArea, this)); + workspace()->raiseClient(this); + } else { + if (m_requestedMaximizeMode == MaximizeRestore) { + updateQuickTileMode(QuickTileFlag::None); + } + if (quickTileMode() != oldQuickTileMode) { + emit quickTileModeChanged(); + } + + if (geometryRestore().isValid()) { + setFrameGeometry(geometryRestore()); + } else { + setFrameGeometry(workspace()->clientArea(PlacementArea, this)); + } + } + + doSetMaximized(); +} + +XdgPopupClient::XdgPopupClient(XdgPopupInterface *shellSurface) + : XdgSurfaceClient(shellSurface->xdgSurface()) + , m_shellSurface(shellSurface) +{ + setDesktop(VirtualDesktopManager::self()->current()); + + connect(shellSurface, &XdgPopupInterface::grabRequested, + this, &XdgPopupClient::handleGrabRequested); + connect(shellSurface, &XdgPopupInterface::initializeRequested, + this, &XdgPopupClient::initialize); + connect(shellSurface, &XdgPopupInterface::destroyed, + this, &XdgPopupClient::destroyClient); + + // The xdg-shell spec states that the parent xdg-surface may be null if it is specified + // via "some other protocol," but we don't support any such protocol yet. Notice that the + // xdg-foreign protocol is only for toplevel surfaces. + + XdgSurfaceInterface *parentShellSurface = shellSurface->parentXdgSurface(); + AbstractClient *parentClient = waylandServer()->findClient(parentShellSurface->surface()); + parentClient->addTransient(this); + setTransientFor(parentClient); +} + +XdgPopupClient::~XdgPopupClient() +{ } -void XdgShellClient::updateColorScheme() +void XdgPopupClient::debug(QDebug &stream) const { - if (m_paletteInterface) { - AbstractClient::updateColorScheme(rules()->checkDecoColor(m_paletteInterface->palette())); - } else { - AbstractClient::updateColorScheme(rules()->checkDecoColor(QString())); - } + stream << this; } -void XdgShellClient::updateMaximizeMode(MaximizeMode maximizeMode) +NET::WindowType XdgPopupClient::windowType(bool direct, int supported_types) const { - if (maximizeMode == m_maximizeMode) { - return; - } + Q_UNUSED(direct) + Q_UNUSED(supported_types) + return NET::Normal; +} - m_maximizeMode = maximizeMode; - updateWindowRules(Rules::MaximizeHoriz | Rules::MaximizeVert | Rules::Position | Rules::Size); +bool XdgPopupClient::hasPopupGrab() const +{ + return m_haveExplicitGrab; +} - emit clientMaximizedStateChanged(this, m_maximizeMode); - emit clientMaximizedStateChanged(this, m_maximizeMode & MaximizeHorizontal, m_maximizeMode & MaximizeVertical); +void XdgPopupClient::popupDone() +{ + m_shellSurface->sendPopupDone(); } -bool XdgShellClient::hasStrut() const +bool XdgPopupClient::isPopupWindow() const { - if (!isShown(true)) { - return false; - } - if (!m_plasmaShellSurface) { - return false; - } - if (m_plasmaShellSurface->role() != PlasmaShellSurfaceInterface::Role::Panel) { - return false; - } - return m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AlwaysVisible; + return true; } -quint32 XdgShellClient::windowId() const +bool XdgPopupClient::isTransient() const { - return m_windowId; + return true; } -void XdgShellClient::updateIcon() +bool XdgPopupClient::isResizable() const { - const QString waylandIconName = QStringLiteral("wayland"); - const QString dfIconName = iconFromDesktopFile(); - const QString iconName = dfIconName.isEmpty() ? waylandIconName : dfIconName; - if (iconName == icon().name()) { - return; - } - setIcon(QIcon::fromTheme(iconName)); + return false; +} + +bool XdgPopupClient::isMovable() const +{ + return false; } -bool XdgShellClient::isTransient() const +bool XdgPopupClient::isMovableAcrossScreens() const { - return m_transient; + return false; } -bool XdgShellClient::hasTransientPlacementHint() const +bool XdgPopupClient::hasTransientPlacementHint() const { - return isTransient() && transientFor() && m_xdgShellPopup; + return true; } -QRect XdgShellClient::transientPlacement(const QRect &bounds) const +static QPoint popupOffset(const QRect &anchorRect, const Qt::Edges anchorEdge, + const Qt::Edges gravity, const QSize popupSize) { - Q_ASSERT(m_xdgShellPopup); + QPoint anchorPoint; + switch (anchorEdge & (Qt::LeftEdge | Qt::RightEdge)) { + case Qt::LeftEdge: + anchorPoint.setX(anchorRect.x()); + break; + case Qt::RightEdge: + anchorPoint.setX(anchorRect.x() + anchorRect.width()); + break; + default: + anchorPoint.setX(qRound(anchorRect.x() + anchorRect.width() / 2.0)); + } + switch (anchorEdge & (Qt::TopEdge | Qt::BottomEdge)) { + case Qt::TopEdge: + anchorPoint.setY(anchorRect.y()); + break; + case Qt::BottomEdge: + anchorPoint.setY(anchorRect.y() + anchorRect.height()); + break; + default: + anchorPoint.setY(qRound(anchorRect.y() + anchorRect.height() / 2.0)); + } + + // calculate where the top left point of the popup will end up with the applied gravity + // gravity indicates direction. i.e if gravitating towards the top the popup's bottom edge + // will next to the anchor point + QPoint popupPosAdjust; + switch (gravity & (Qt::LeftEdge | Qt::RightEdge)) { + case Qt::LeftEdge: + popupPosAdjust.setX(-popupSize.width()); + break; + case Qt::RightEdge: + popupPosAdjust.setX(0); + break; + default: + popupPosAdjust.setX(qRound(-popupSize.width() / 2.0)); + } + switch (gravity & (Qt::TopEdge | Qt::BottomEdge)) { + case Qt::TopEdge: + popupPosAdjust.setY(-popupSize.height()); + break; + case Qt::BottomEdge: + popupPosAdjust.setY(0); + break; + default: + popupPosAdjust.setY(qRound(-popupSize.height() / 2.0)); + } + + return anchorPoint + popupPosAdjust; +} - QRect anchorRect; - Qt::Edges anchorEdge; - Qt::Edges gravity; - QPoint offset; - PositionerConstraints constraintAdjustments; - QSize size = frameGeometry().size(); +QRect XdgPopupClient::transientPlacement(const QRect &bounds) const +{ + const XdgPositioner positioner = m_shellSurface->positioner(); + const QSize desiredSize = isUnmapped() ? positioner.size() : size(); - const QPoint parentClientPos = transientFor()->pos() + transientFor()->clientPos(); + const QPoint parentPosition = transientFor()->framePosToClientPos(transientFor()->pos()); // returns if a target is within the supplied bounds, optional edges argument states which side to check auto inBounds = [bounds](const QRect &target, Qt::Edges edges = Qt::LeftEdge | Qt::RightEdge | Qt::TopEdge | Qt::BottomEdge) -> bool { if (edges & Qt::LeftEdge && target.left() < bounds.left()) { return false; } if (edges & Qt::TopEdge && target.top() < bounds.top()) { return false; } if (edges & Qt::RightEdge && target.right() > bounds.right()) { //normal QRect::right issue cancels out return false; } if (edges & Qt::BottomEdge && target.bottom() > bounds.bottom()) { return false; } return true; }; - anchorRect = m_xdgShellPopup->anchorRect(); - anchorEdge = m_xdgShellPopup->anchorEdge(); - gravity = m_xdgShellPopup->gravity(); - offset = m_xdgShellPopup->anchorOffset(); - constraintAdjustments = m_xdgShellPopup->constraintAdjustments(); - if (!size.isValid()) { - size = m_xdgShellPopup->initialSize(); - } - - QRect popupRect(popupOffset(anchorRect, anchorEdge, gravity, size) + offset + parentClientPos, size); + QRect popupRect(popupOffset(positioner.anchorRect(), positioner.anchorEdges(), positioner.gravityEdges(), desiredSize) + positioner.offset() + parentPosition, desiredSize); //if that fits, we don't need to do anything if (inBounds(popupRect)) { return popupRect; } //otherwise apply constraint adjustment per axis in order XDG Shell Popup states - if (constraintAdjustments & PositionerConstraint::FlipX) { + if (positioner.flipConstraintAdjustments() & Qt::Horizontal) { if (!inBounds(popupRect, Qt::LeftEdge | Qt::RightEdge)) { //flip both edges (if either bit is set, XOR both) - auto flippedAnchorEdge = anchorEdge; + auto flippedAnchorEdge = positioner.anchorEdges(); if (flippedAnchorEdge & (Qt::LeftEdge | Qt::RightEdge)) { flippedAnchorEdge ^= (Qt::LeftEdge | Qt::RightEdge); } - auto flippedGravity = gravity; + auto flippedGravity = positioner.gravityEdges(); if (flippedGravity & (Qt::LeftEdge | Qt::RightEdge)) { flippedGravity ^= (Qt::LeftEdge | Qt::RightEdge); } - auto flippedPopupRect = QRect(popupOffset(anchorRect, flippedAnchorEdge, flippedGravity, size) + offset + parentClientPos, size); + auto flippedPopupRect = QRect(popupOffset(positioner.anchorRect(), flippedAnchorEdge, flippedGravity, desiredSize) + positioner.offset() + parentPosition, desiredSize); //if it still doesn't fit we should continue with the unflipped version if (inBounds(flippedPopupRect, Qt::LeftEdge | Qt::RightEdge)) { popupRect.moveLeft(flippedPopupRect.left()); } } } - if (constraintAdjustments & PositionerConstraint::SlideX) { + if (positioner.slideConstraintAdjustments() & Qt::Horizontal) { if (!inBounds(popupRect, Qt::LeftEdge)) { popupRect.moveLeft(bounds.left()); } if (!inBounds(popupRect, Qt::RightEdge)) { popupRect.moveRight(bounds.right()); } } - if (constraintAdjustments & PositionerConstraint::ResizeX) { + if (positioner.resizeConstraintAdjustments() & Qt::Horizontal) { QRect unconstrainedRect = popupRect; if (!inBounds(unconstrainedRect, Qt::LeftEdge)) { unconstrainedRect.setLeft(bounds.left()); } if (!inBounds(unconstrainedRect, Qt::RightEdge)) { unconstrainedRect.setRight(bounds.right()); } if (unconstrainedRect.isValid()) { popupRect = unconstrainedRect; } } - if (constraintAdjustments & PositionerConstraint::FlipY) { + if (positioner.flipConstraintAdjustments() & Qt::Vertical) { if (!inBounds(popupRect, Qt::TopEdge | Qt::BottomEdge)) { //flip both edges (if either bit is set, XOR both) - auto flippedAnchorEdge = anchorEdge; + auto flippedAnchorEdge = positioner.anchorEdges(); if (flippedAnchorEdge & (Qt::TopEdge | Qt::BottomEdge)) { flippedAnchorEdge ^= (Qt::TopEdge | Qt::BottomEdge); } - auto flippedGravity = gravity; + auto flippedGravity = positioner.gravityEdges(); if (flippedGravity & (Qt::TopEdge | Qt::BottomEdge)) { flippedGravity ^= (Qt::TopEdge | Qt::BottomEdge); } - auto flippedPopupRect = QRect(popupOffset(anchorRect, flippedAnchorEdge, flippedGravity, size) + offset + parentClientPos, size); + auto flippedPopupRect = QRect(popupOffset(positioner.anchorRect(), flippedAnchorEdge, flippedGravity, desiredSize) + positioner.offset() + parentPosition, desiredSize); //if it still doesn't fit we should continue with the unflipped version if (inBounds(flippedPopupRect, Qt::TopEdge | Qt::BottomEdge)) { popupRect.moveTop(flippedPopupRect.top()); } } } - if (constraintAdjustments & PositionerConstraint::SlideY) { + if (positioner.slideConstraintAdjustments() & Qt::Vertical) { if (!inBounds(popupRect, Qt::TopEdge)) { popupRect.moveTop(bounds.top()); } if (!inBounds(popupRect, Qt::BottomEdge)) { popupRect.moveBottom(bounds.bottom()); } } - if (constraintAdjustments & PositionerConstraint::ResizeY) { + if (positioner.resizeConstraintAdjustments() & Qt::Vertical) { QRect unconstrainedRect = popupRect; if (!inBounds(unconstrainedRect, Qt::TopEdge)) { unconstrainedRect.setTop(bounds.top()); } if (!inBounds(unconstrainedRect, Qt::BottomEdge)) { unconstrainedRect.setBottom(bounds.bottom()); } if (unconstrainedRect.isValid()) { popupRect = unconstrainedRect; } } return popupRect; } -QPoint XdgShellClient::popupOffset(const QRect &anchorRect, const Qt::Edges anchorEdge, const Qt::Edges gravity, const QSize popupSize) const -{ - QPoint anchorPoint; - switch (anchorEdge & (Qt::LeftEdge | Qt::RightEdge)) { - case Qt::LeftEdge: - anchorPoint.setX(anchorRect.x()); - break; - case Qt::RightEdge: - anchorPoint.setX(anchorRect.x() + anchorRect.width()); - break; - default: - anchorPoint.setX(qRound(anchorRect.x() + anchorRect.width() / 2.0)); - } - switch (anchorEdge & (Qt::TopEdge | Qt::BottomEdge)) { - case Qt::TopEdge: - anchorPoint.setY(anchorRect.y()); - break; - case Qt::BottomEdge: - anchorPoint.setY(anchorRect.y() + anchorRect.height()); - break; - default: - anchorPoint.setY(qRound(anchorRect.y() + anchorRect.height() / 2.0)); - } - - // calculate where the top left point of the popup will end up with the applied gravity - // gravity indicates direction. i.e if gravitating towards the top the popup's bottom edge - // will next to the anchor point - QPoint popupPosAdjust; - switch (gravity & (Qt::LeftEdge | Qt::RightEdge)) { - case Qt::LeftEdge: - popupPosAdjust.setX(-popupSize.width()); - break; - case Qt::RightEdge: - popupPosAdjust.setX(0); - break; - default: - popupPosAdjust.setX(qRound(-popupSize.width() / 2.0)); - } - switch (gravity & (Qt::TopEdge | Qt::BottomEdge)) { - case Qt::TopEdge: - popupPosAdjust.setY(-popupSize.height()); - break; - case Qt::BottomEdge: - popupPosAdjust.setY(0); - break; - default: - popupPosAdjust.setY(qRound(-popupSize.height() / 2.0)); - } - - return anchorPoint + popupPosAdjust; -} - -void XdgShellClient::doResizeSync() -{ - requestGeometry(moveResizeGeometry()); -} - -QMatrix4x4 XdgShellClient::inputTransformation() const +bool XdgPopupClient::isCloseable() const { - QMatrix4x4 matrix; - matrix.translate(-m_bufferGeometry.x(), -m_bufferGeometry.y()); - return matrix; + return false; } -void XdgShellClient::installServerSideDecoration(KWaylandServer::ServerSideDecorationInterface *deco) +void XdgPopupClient::closeWindow() { - if (m_serverDecoration == deco) { - return; - } - m_serverDecoration = deco; - connect(m_serverDecoration, &ServerSideDecorationInterface::destroyed, this, - [this] { - m_serverDecoration = nullptr; - if (m_closing || !Workspace::self()) { - return; - } - if (!m_unmapped) { - // maybe delay to next event cycle in case the XdgShellClient is getting destroyed, too - updateDecoration(true); - } - } - ); - if (!m_unmapped) { - updateDecoration(true); - } - connect(m_serverDecoration, &ServerSideDecorationInterface::modeRequested, this, - [this] (ServerSideDecorationManagerInterface::Mode mode) { - const bool changed = mode != m_serverDecoration->mode(); - if (changed && !m_unmapped) { - updateDecoration(false); - } - } - ); } -void XdgShellClient::installXdgDecoration(XdgDecorationInterface *deco) +void XdgPopupClient::updateColorScheme() { - Q_ASSERT(m_xdgShellToplevel); - - m_xdgDecoration = deco; - - connect(m_xdgDecoration, &QObject::destroyed, this, - [this] { - m_xdgDecoration = nullptr; - if (m_closing || !Workspace::self()) { - return; - } - updateDecoration(true); - } - ); - - connect(m_xdgDecoration, &XdgDecorationInterface::modeRequested, this, - [this] () { - //force is true as we must send a new configure response - updateDecoration(false, true); - }); + AbstractClient::updateColorScheme(QString()); } -bool XdgShellClient::shouldExposeToWindowManagement() +bool XdgPopupClient::noBorder() const { - if (isLockScreen()) { - return false; - } - if (m_xdgShellPopup) { - return false; - } return true; } -KWaylandServer::XdgShellSurfaceInterface::States XdgShellClient::xdgSurfaceStates() const +bool XdgPopupClient::userCanSetNoBorder() const { - XdgShellSurfaceInterface::States states; - if (isActive()) { - states |= XdgShellSurfaceInterface::State::Activated; - } - if (isFullScreen()) { - states |= XdgShellSurfaceInterface::State::Fullscreen; - } - if (m_requestedMaximizeMode == MaximizeMode::MaximizeFull) { - states |= XdgShellSurfaceInterface::State::Maximized; - } - if (isResize()) { - states |= XdgShellSurfaceInterface::State::Resizing; - } - return states; + return false; } -void XdgShellClient::doMinimize() +void XdgPopupClient::setNoBorder(bool set) { - if (isMinimized()) { - workspace()->clientHidden(this); - } else { - emit windowShown(this); - } - workspace()->updateMinimizedOfTransients(this); + Q_UNUSED(set) } -void XdgShellClient::showOnScreenEdge() +void XdgPopupClient::updateDecoration(bool check_workspace_pos, bool force) { - if (!m_plasmaShellSurface || m_unmapped) { - return; - } - hideClient(false); - workspace()->raiseClient(this); - if (m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AutoHide) { - m_plasmaShellSurface->showAutoHidingPanel(); - } + Q_UNUSED(check_workspace_pos) + Q_UNUSED(force) } -bool XdgShellClient::dockWantsInput() const +void XdgPopupClient::showOnScreenEdge() { - if (m_plasmaShellSurface) { - if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Panel) { - return m_plasmaShellSurface->panelTakesFocus(); - } - } - return false; } -void XdgShellClient::killWindow() +bool XdgPopupClient::supportsWindowRules() const { - if (!surface()) { - return; - } - auto c = surface()->client(); - if (c->processId() == getpid() || c->processId() == 0) { - c->destroy(); - return; - } - ::kill(c->processId(), SIGTERM); - // give it time to terminate and only if terminate fails, try destroy Wayland connection - QTimer::singleShot(5000, c, &ClientConnection::destroy); + return false; } -bool XdgShellClient::isLocalhost() const +bool XdgPopupClient::wantsInput() const { - return true; + return false; } -bool XdgShellClient::hasPopupGrab() const +void XdgPopupClient::takeFocus() { - return m_hasPopupGrab; } -void XdgShellClient::popupDone() +bool XdgPopupClient::acceptsFocus() const { - if (m_xdgShellPopup) { - m_xdgShellPopup->popupDone(); - } + return false; } -void XdgShellClient::updateClientOutputs() +XdgSurfaceConfigure *XdgPopupClient::sendRoleConfigure() const { - QVector clientOutputs; - const auto outputs = waylandServer()->display()->outputs(); - for (const auto output : outputs) { - if (output->isEnabled()) { - const QRect outputGeometry(output->globalPosition(), output->pixelSize() / output->scale()); - if (frameGeometry().intersects(outputGeometry)) { - clientOutputs << output; - } - } - } - surface()->setOutputs(clientOutputs); -} + const QPoint parentPosition = transientFor()->framePosToClientPos(transientFor()->pos()); + const QPoint popupPosition = requestedPos() - parentPosition; -bool XdgShellClient::isPopupWindow() const -{ - if (Toplevel::isPopupWindow()) { - return true; - } - if (m_xdgShellPopup != nullptr) { - return true; - } - return false; -} + const quint32 serial = m_shellSurface->sendConfigure(QRect(popupPosition, requestedClientSize())); -bool XdgShellClient::supportsWindowRules() const -{ - if (m_plasmaShellSurface) { - return false; - } - return m_xdgShellToplevel; -} + XdgSurfaceConfigure *configureEvent = new XdgSurfaceConfigure(); + configureEvent->position = requestedPos(); + configureEvent->size = requestedSize(); + configureEvent->serial = serial; -QRect XdgShellClient::adjustMoveGeometry(const QRect &rect) const -{ - QRect geometry = rect; - geometry.moveTopLeft(moveResizeGeometry().topLeft()); - return geometry; + return configureEvent; } -QRect XdgShellClient::adjustResizeGeometry(const QRect &rect) const +void XdgPopupClient::handleGrabRequested(SeatInterface *seat, quint32 serial) { - QRect geometry = rect; - - // We need to adjust frame geometry because configure events carry the maximum window geometry - // size. A client that has aspect ratio can attach a buffer with smaller size than the one in - // a configure event. - switch (moveResizePointerMode()) { - case PositionTopLeft: - geometry.moveRight(moveResizeGeometry().right()); - geometry.moveBottom(moveResizeGeometry().bottom()); - break; - case PositionTop: - case PositionTopRight: - geometry.moveLeft(moveResizeGeometry().left()); - geometry.moveBottom(moveResizeGeometry().bottom()); - break; - case PositionRight: - case PositionBottomRight: - case PositionBottom: - geometry.moveLeft(moveResizeGeometry().left()); - geometry.moveTop(moveResizeGeometry().top()); - break; - case PositionBottomLeft: - case PositionLeft: - geometry.moveRight(moveResizeGeometry().right()); - geometry.moveTop(moveResizeGeometry().top()); - break; - case PositionCenter: - Q_UNREACHABLE(); - } - - return geometry; + Q_UNUSED(seat) + Q_UNUSED(serial) + m_haveExplicitGrab = true; } -void XdgShellClient::ping(PingReason reason) +void XdgPopupClient::initialize() { - Q_ASSERT(m_xdgShellToplevel); + const QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop()); + placeIn(area); - XdgShellInterface *shell = static_cast(m_xdgShellToplevel->global()); - const quint32 serial = shell->ping(m_xdgShellToplevel); - m_pingSerials.insert(serial, reason); + scheduleConfigure(); } -} +} // namespace KWin diff --git a/xdgshellclient.h b/xdgshellclient.h index 66a3b3fbe..95739c501 100644 --- a/xdgshellclient.h +++ b/xdgshellclient.h @@ -1,264 +1,296 @@ /******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2015 Martin Gräßlin Copyright (C) 2018 David Edmundson Copyright (C) 2019 Vlad Zahorodnii This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General 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_client.h" +#include "waylandclient.h" #include +#include +#include + namespace KWaylandServer { -class ServerSideDecorationInterface; -class ServerSideDecorationPaletteInterface; class AppMenuInterface; class PlasmaShellSurfaceInterface; -class XdgDecorationInterface; +class ServerSideDecorationInterface; +class ServerSideDecorationPaletteInterface; +class XdgToplevelDecorationV1Interface; } namespace KWin { -/** - * @brief The reason for which the server pinged a client surface - */ -enum class PingReason { - CloseWindow = 0, - FocusWindow +class XdgSurfaceConfigure +{ +public: + enum ConfigureField { + PositionField = 0x1, + SizeField = 0x2, + }; + Q_DECLARE_FLAGS(ConfigureFields, ConfigureField) + + ConfigureFields presentFields; + QPoint position; + QSize size; + qreal serial; }; -class KWIN_EXPORT XdgShellClient : public AbstractClient +class XdgSurfaceClient : public WaylandClient { Q_OBJECT public: - XdgShellClient(KWaylandServer::XdgShellSurfaceInterface *surface); - XdgShellClient(KWaylandServer::XdgShellPopupInterface *surface); - ~XdgShellClient() override; + explicit XdgSurfaceClient(KWaylandServer::XdgSurfaceInterface *shellSurface); + ~XdgSurfaceClient() override; QRect inputGeometry() const override; QRect bufferGeometry() const override; - QStringList activities() const override; - QPoint clientContentPos() const override; QSize clientSize() const override; + QMatrix4x4 inputTransformation() const override; + void setFrameGeometry(const QRect &rect, ForceGeometry_t force = NormalGeometrySet) override; + using AbstractClient::move; + void move(int x, int y, ForceGeometry_t force = NormalGeometrySet) override; + bool isShown(bool shaded_is_shown) const override; + bool isHiddenInternal() const override; + void hideClient(bool hide) override; + void destroyClient() override; + + QRect frameRectToBufferRect(const QRect &rect) const; + QRect requestedFrameGeometry() const; + QPoint requestedPos() const; + QSize requestedSize() const; + QRect requestedClientGeometry() const; + QSize requestedClientSize() const; + QRect clientGeometry() const; + bool isClosing() const; + bool isHidden() const; + bool isUnmapped() const; + +Q_SIGNALS: + void windowMapped(); + void windowUnmapped(); + +protected: + void addDamage(const QRegion &damage) override; + + virtual XdgSurfaceConfigure *sendRoleConfigure() const = 0; + virtual void handleRoleCommit(); + virtual bool stateCompare() const; + + XdgSurfaceConfigure *lastAcknowledgedConfigure() const; + void scheduleConfigure(); + void sendConfigure(); + void requestGeometry(const QRect &rect); + void updateGeometry(const QRect &rect); + +private: + void handleConfigureAcknowledged(quint32 serial); + void handleCommit(); + void handleNextWindowGeometry(); + bool haveNextWindowGeometry() const; + void setHaveNextWindowGeometry(); + void resetHaveNextWindowGeometry(); + QRect adjustMoveResizeGeometry(const QRect &rect) const; + void updateGeometryRestoreHack(); + void updateDepth(); + void internalShow(); + void internalHide(); + void internalMap(); + void internalUnmap(); + void cleanGrouping(); + void cleanTabBox(); + + KWaylandServer::XdgSurfaceInterface *m_shellSurface; + QTimer *m_configureTimer; + QQueue m_configureEvents; + QScopedPointer m_lastAcknowledgedConfigure; + QRect m_windowGeometry; + QRect m_requestedFrameGeometry; + QRect m_bufferGeometry; + QRect m_requestedClientGeometry; + QRect m_clientGeometry; + bool m_isClosing = false; + bool m_isHidden = false; + bool m_isUnmapped = true; + bool m_haveNextWindowGeometry = false; +}; + +class XdgToplevelConfigure final : public XdgSurfaceConfigure +{ +public: + KWaylandServer::XdgToplevelInterface::States states; +}; + +class XdgToplevelClient final : public XdgSurfaceClient +{ + Q_OBJECT + + enum class PingReason { CloseWindow, FocusWindow }; + +public: + explicit XdgToplevelClient(KWaylandServer::XdgToplevelInterface *shellSurface); + ~XdgToplevelClient() override; + + void debug(QDebug &stream) const override; + NET::WindowType windowType(bool direct = false, int supported_types = 0) const override; + MaximizeMode maximizeMode() const override; + MaximizeMode requestedMaximizeMode() const override; QSize minSize() const override; QSize maxSize() const override; - QRect transparentRect() const override; - NET::WindowType windowType(bool direct = false, int supported_types = 0) const override; - void debug(QDebug &stream) const override; - double opacity() const override; - void setOpacity(double opacity) override; - QByteArray windowRole() const override; - void blockActivityUpdates(bool b = true) override; - QString captionNormal() const override; - QString captionSuffix() const override; - void closeWindow() override; - AbstractClient *findModal(bool allow_itself = false) override; + bool isFullScreen() const override; + bool isMovableAcrossScreens() const override; + bool isMovable() const override; + bool isResizable() const override; bool isCloseable() const override; bool isFullScreenable() const override; - bool isFullScreen() const override; bool isMaximizable() const override; bool isMinimizable() const override; - bool isMovable() const override; - bool isMovableAcrossScreens() const override; - bool isResizable() const override; - bool isShown(bool shaded_is_shown) const override; - bool isHiddenInternal() const override; - void hideClient(bool hide) override; - MaximizeMode maximizeMode() const override; - MaximizeMode requestedMaximizeMode() const override; + bool isTransient() const override; + bool userCanSetFullScreen() const override; + bool userCanSetNoBorder() const override; bool noBorder() const override; - void setFullScreen(bool set, bool user = true) override; void setNoBorder(bool set) override; void updateDecoration(bool check_workspace_pos, bool force = false) override; - void setOnAllActivities(bool set) override; + void updateColorScheme() override; + bool supportsWindowRules() const override; void takeFocus() override; - bool userCanSetFullScreen() const override; - bool userCanSetNoBorder() const override; bool wantsInput() const override; bool dockWantsInput() const override; - void resizeWithChecks(const QSize &size, ForceGeometry_t force = NormalGeometrySet) override; - void setFrameGeometry(const QRect &rect, ForceGeometry_t force = NormalGeometrySet) override; bool hasStrut() const override; - quint32 windowId() const override; - pid_t pid() const override; - bool isLockScreen() const override; - bool isInputMethod() const override; - bool isInitialPositionSet() const override; - bool isTransient() const override; - bool hasTransientPlacementHint() const override; - QRect transientPlacement(const QRect &bounds) const override; - QMatrix4x4 inputTransformation() const override; void showOnScreenEdge() override; - bool hasPopupGrab() const override; - void popupDone() override; - void updateColorScheme() override; - bool isPopupWindow() const override; - void killWindow() override; - bool isLocalhost() const override; - bool supportsWindowRules() const override; - void destroyClient() override; + bool isInitialPositionSet() const override; + void setFullScreen(bool set, bool user) override; + void closeWindow() override; - void installPlasmaShellSurface(KWaylandServer::PlasmaShellSurfaceInterface *surface); - void installServerSideDecoration(KWaylandServer::ServerSideDecorationInterface *decoration); - void installAppMenu(KWaylandServer::AppMenuInterface *appmenu); + void installAppMenu(KWaylandServer::AppMenuInterface *appMenu); + void installServerDecoration(KWaylandServer::ServerSideDecorationInterface *decoration); void installPalette(KWaylandServer::ServerSideDecorationPaletteInterface *palette); - void installXdgDecoration(KWaylandServer::XdgDecorationInterface *decoration); + void installPlasmaShellSurface(KWaylandServer::PlasmaShellSurfaceInterface *shellSurface); + void installXdgDecoration(KWaylandServer::XdgToplevelDecorationV1Interface *decoration); protected: - void addDamage(const QRegion &damage) override; - bool belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const override; - void doSetActive() override; - bool belongsToDesktop() const override; - Layer layerForDock() const override; - void changeMaximize(bool horizontal, bool vertical, bool adjust) override; + XdgSurfaceConfigure *sendRoleConfigure() const override; + void handleRoleCommit() override; + void doMinimize() override; void doResizeSync() override; + void doSetActive() override; + void doSetFullScreen(); + void doSetMaximized(); + bool doStartMoveResize() override; + void doFinishMoveResize() override; bool acceptsFocus() const override; - void doMinimize() override; - void updateCaption() override; - void doMove(int x, int y) override; + void changeMaximize(bool horizontal, bool vertical, bool adjust) override; + Layer layerForDock() const override; + bool stateCompare() const override; -private Q_SLOTS: - void handleConfigureAcknowledged(quint32 serial); - void handleTransientForChanged(); - void handleWindowClassChanged(const QByteArray &windowClass); - void handleWindowGeometryChanged(const QRect &windowGeometry); - void handleWindowTitleChanged(const QString &title); +private: + void handleWindowTitleChanged(); + void handleWindowClassChanged(); + void handleWindowMenuRequested(KWaylandServer::SeatInterface *seat, + const QPoint &surfacePos, quint32 serial); void handleMoveRequested(KWaylandServer::SeatInterface *seat, quint32 serial); - void handleResizeRequested(KWaylandServer::SeatInterface *seat, quint32 serial, Qt::Edges edges); + void handleResizeRequested(KWaylandServer::SeatInterface *seat, Qt::Edges, quint32 serial); + void handleStatesAcknowledged(const KWaylandServer::XdgToplevelInterface::States &states); + void handleMaximizeRequested(); + void handleUnmaximizeRequested(); + void handleFullscreenRequested(KWaylandServer::OutputInterface *output); + void handleUnfullscreenRequested(); void handleMinimizeRequested(); - void handleMaximizeRequested(bool maximized); - void handleFullScreenRequested(bool fullScreen, KWaylandServer::OutputInterface *output); - void handleWindowMenuRequested(KWaylandServer::SeatInterface *seat, quint32 serial, const QPoint &surfacePos); - void handleGrabRequested(KWaylandServer::SeatInterface *seat, quint32 serial); - void handlePingDelayed(quint32 serial); + void handleTransientForChanged(); + void handleForeignTransientForChanged(KWaylandServer::SurfaceInterface *child); void handlePingTimeout(quint32 serial); + void handlePingDelayed(quint32 serial); void handlePongReceived(quint32 serial); - void handleCommitted(); - -private: - /** - * Called when the shell is created. - */ - void init(); - /** - * Called for the XDG case when the shell surface is committed to the surface. - * At this point all initial properties should have been set by the client. - */ - void finishInit(); - void createWindowId(); - void updateIcon(); - bool shouldExposeToWindowManagement(); - void updateClientOutputs(); - KWaylandServer::XdgShellSurfaceInterface::States xdgSurfaceStates() const; - void updateShowOnScreenEdge(); + void initialize(); void updateMaximizeMode(MaximizeMode maximizeMode); - // called on surface commit and processes all m_pendingConfigureRequests up to m_lastAckedConfigureReqest - void updatePendingGeometry(); - QPoint popupOffset(const QRect &anchorRect, const Qt::Edges anchorEdge, const Qt::Edges gravity, const QSize popupSize) const; - void requestGeometry(const QRect &rect); - void doSetGeometry(const QRect &rect); - void unmap(); - void markAsMapped(); - QRect determineBufferGeometry() const; - void ping(PingReason reason); - static void deleteClient(XdgShellClient *c); - - QRect adjustMoveGeometry(const QRect &rect) const; - QRect adjustResizeGeometry(const QRect &rect) const; - - KWaylandServer::XdgShellSurfaceInterface *m_xdgShellToplevel; - KWaylandServer::XdgShellPopupInterface *m_xdgShellPopup; - - QRect m_bufferGeometry; - QRect m_windowGeometry; - bool m_hasWindowGeometry = false; - - // last size we requested or empty if we haven't sent an explicit request to the client - // if empty the client should choose their own default size - QSize m_requestedClientSize = QSize(0, 0); - - struct PendingConfigureRequest { - //note for wl_shell we have no serial, so serialId and m_lastAckedConfigureRequest will always be 0 - //meaning we treat a surface commit as having processed all requests - quint32 serialId = 0; - // position to apply after a resize operation has been completed - QPoint positionAfterResize; - MaximizeMode maximizeMode; - }; - QVector m_pendingConfigureRequests; - quint32 m_lastAckedConfigureRequest = 0; - - //mode in use by the current buffer - MaximizeMode m_maximizeMode = MaximizeRestore; - //mode we currently want to be, could be pending on client updating, could be not sent yet - MaximizeMode m_requestedMaximizeMode = MaximizeRestore; + void updateFullScreenMode(bool set); + void updateShowOnScreenEdge(); + void setupWindowManagementIntegration(); + void setupPlasmaShellIntegration(); + void sendPing(PingReason reason); - QRect m_geomFsRestore; //size and position of the window before it was set to fullscreen - bool m_closing = false; - quint32 m_windowId = 0; - bool m_unmapped = true; - NET::WindowType m_windowType = NET::Normal; QPointer m_plasmaShellSurface; QPointer m_appMenuInterface; QPointer m_paletteInterface; - KWaylandServer::ServerSideDecorationInterface *m_serverDecoration = nullptr; - KWaylandServer::XdgDecorationInterface *m_xdgDecoration = nullptr; + QPointer m_serverDecoration; + QPointer m_xdgDecoration; + KWaylandServer::XdgToplevelInterface *m_shellSurface; + KWaylandServer::XdgToplevelInterface::States m_requestedStates; + KWaylandServer::XdgToplevelInterface::States m_acknowledgedStates; + QMap m_pings; + QRect m_fullScreenGeometryRestore; + NET::WindowType m_windowType = NET::Normal; + MaximizeMode m_maximizeMode = MaximizeRestore; + MaximizeMode m_requestedMaximizeMode = MaximizeRestore; + bool m_isFullScreen = false; bool m_userNoBorder = false; - bool m_fullScreen = false; - bool m_transient = false; - bool m_hidden = false; - bool m_hasPopupGrab = false; - qreal m_opacity = 1.0; - - class RequestGeometryBlocker { //TODO rename ConfigureBlocker when this class is Xdg only - public: - RequestGeometryBlocker(XdgShellClient *client) - : m_client(client) - { - m_client->m_requestGeometryBlockCounter++; - } - ~RequestGeometryBlocker() - { - m_client->m_requestGeometryBlockCounter--; - if (m_client->m_requestGeometryBlockCounter == 0) { - m_client->requestGeometry(m_client->m_blockedRequestGeometry); - } - } - private: - XdgShellClient *m_client; - }; - friend class RequestGeometryBlocker; - int m_requestGeometryBlockCounter = 0; - QRect m_blockedRequestGeometry; - QString m_caption; - QString m_captionSuffix; - QHash m_pingSerials; + bool m_isTransient = false; +}; + +class XdgPopupClient final : public XdgSurfaceClient +{ + Q_OBJECT + +public: + explicit XdgPopupClient(KWaylandServer::XdgPopupInterface *shellSurface); + ~XdgPopupClient() override; + + void debug(QDebug &stream) const override; + NET::WindowType windowType(bool direct = false, int supported_types = 0) const override; + bool hasPopupGrab() const override; + void popupDone() override; + bool isPopupWindow() const override; + bool isTransient() const override; + bool isResizable() const override; + bool isMovable() const override; + bool isMovableAcrossScreens() const override; + bool hasTransientPlacementHint() const override; + QRect transientPlacement(const QRect &bounds) const override; + bool isCloseable() const override; + void closeWindow() override; + void updateColorScheme() override; + bool noBorder() const override; + bool userCanSetNoBorder() const override; + void setNoBorder(bool set) override; + void updateDecoration(bool check_workspace_pos, bool force = false) override; + void showOnScreenEdge() override; + bool wantsInput() const override; + void takeFocus() override; + bool supportsWindowRules() const override; - bool m_isInitialized = false; +protected: + bool acceptsFocus() const override; + XdgSurfaceConfigure *sendRoleConfigure() const override; - friend class Workspace; +private: + void handleGrabRequested(KWaylandServer::SeatInterface *seat, quint32 serial); + void initialize(); + + KWaylandServer::XdgPopupInterface *m_shellSurface; + bool m_haveExplicitGrab = false; }; -} +} // namespace KWin -Q_DECLARE_METATYPE(KWin::XdgShellClient *) +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::XdgSurfaceConfigure::ConfigureFields)