فهرست منبع

suppressed log on kde. wallpaper doesnt minimize on super+d now

UwU 4 ماه پیش
والد
کامیت
dabbb940fc

+ 20 - 0
CMakeLists.txt

@@ -151,6 +151,7 @@ if(BUILD_TESTING)
 endif()
 # try to enable wayland builds when possible
 pkg_check_modules(WAYLAND_SUPPORT wayland-cursor wayland-protocols egl wayland-egl)
+option(ENABLE_KDE_EXPERIMENTAL_FEATURES "Enable KDE-specific Wayland fullscreen detection" OFF)
 
 if(WAYLAND_SUPPORT_FOUND)
     find_program(WaylandScanner NAMES wayland-scanner)
@@ -210,6 +211,21 @@ if(WAYLAND_SUPPORT_FOUND)
         ${WAYLAND_OUTPUT_DIR}/wlr-layer-shell-unstable-v1-protocol.h
         ${WAYLAND_OUTPUT_DIR}/xdg-output-unstable-v1-protocol.c
         ${WAYLAND_OUTPUT_DIR}/xdg-output-unstable-v1-protocol.h)
+
+    if(ENABLE_KDE_EXPERIMENTAL_FEATURES)
+        pkg_check_modules(DBUS REQUIRED dbus-1)
+        include_directories(${DBUS_INCLUDE_DIRS})
+        list(APPEND WAYLAND_LIBRARIES ${DBUS_LIBRARIES})
+        add_custom_command(OUTPUT ${WAYLAND_OUTPUT_DIR}/plasma-shell-protocol.h
+            COMMAND ${WaylandScanner} client-header ${CMAKE_CURRENT_SOURCE_DIR}/protocols/plasma-shell.xml ${WAYLAND_OUTPUT_DIR}/plasma-shell-protocol.h)
+        add_custom_command(OUTPUT ${WAYLAND_OUTPUT_DIR}/plasma-shell-protocol.c
+            COMMAND ${WaylandScanner} private-code ${CMAKE_CURRENT_SOURCE_DIR}/protocols/plasma-shell.xml ${WAYLAND_OUTPUT_DIR}/plasma-shell-protocol.c)
+        list(APPEND WAYLAND_SOURCES
+            src/WallpaperEngine/Render/Drivers/Detectors/KDEWaylandFullScreenDetector.cpp
+            src/WallpaperEngine/Render/Drivers/Detectors/KDEWaylandFullScreenDetector.h
+            ${WAYLAND_OUTPUT_DIR}/plasma-shell-protocol.c
+            ${WAYLAND_OUTPUT_DIR}/plasma-shell-protocol.h)
+    endif()
 endif()
 
 if(X11_FOUND)
@@ -590,6 +606,10 @@ if(WAYLAND_SUPPORT_FOUND)
     target_compile_definitions(linux-wallpaperengine-lib PUBLIC ENABLE_WAYLAND)
 endif()
 
+if(WAYLAND_SUPPORT_FOUND AND ENABLE_KDE_EXPERIMENTAL_FEATURES)
+    target_compile_definitions(linux-wallpaperengine-lib PUBLIC ENABLE_KDE_EXPERIMENTAL_FEATURES)
+endif()
+
 if(WAYLAND_SUPPORT_FOUND AND NOT X11_SUPPORT_FOUND)
     target_compile_definitions(linux-wallpaperengine-lib PUBLIC GLEW_EGL)
 endif()

+ 16 - 0
README.md

@@ -138,6 +138,22 @@ cmake -DCMAKE_BUILD_TYPE='Release' ..
 make
 ```
 
+> ⚠️ Note for KDE Plasma users:
+
+KDE Plasma (Wayland) users who intend to use the “pause on fullscreen” feature are required to install an additional script from the [KWin Maximize Detector](https://github.com/LS-FCEFyN/Maximize-Detector) repository. They must also replace:
+
+```bash
+cmake -DCMAKE_BUILD_TYPE='Release' ..
+```
+
+With:
+
+```bash
+cmake -DCMAKE_BUILD_TYPE='Release' -DENABLE_KDE_EXPERIMENTAL_FEATURES=ON ..
+```
+
+
+
 Once the build process is finished, this should create a new `output` folder containing the app and all the required
 support files to run.
 

+ 180 - 0
protocols/plasma-shell.xml

@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="plasma_shell">
+  <copyright><![CDATA[
+    SPDX-FileCopyrightText: 2013-2014 Pier Luigi Fiorini
+
+    SPDX-License-Identifier: LGPL-2.1-or-later
+  ]]></copyright>
+
+  <interface name="org_kde_plasma_shell" version="8">
+    <description summary="create shell windows and helpers">
+      This interface is used by KF5 powered Wayland shells to communicate with
+      the compositor and can only be bound one time.
+
+      Warning! The protocol described in this file is a desktop environment
+      implementation detail. Regular clients must not use this protocol.
+      Backward incompatible changes may be added without bumping the major
+      version of the extension.
+    </description>
+
+    <request name="get_surface">
+      <description summary="create a shell surface from a surface">
+        Create a shell surface for an existing surface.
+
+        Only one shell surface can be associated with a given
+        surface.
+      </description>
+      <arg name="id" type="new_id" interface="org_kde_plasma_surface"/>
+      <arg name="surface" type="object" interface="wl_surface"/>
+    </request>
+  </interface>
+
+  <interface name="org_kde_plasma_surface" version="8">
+    <description summary="metadata interface">
+      An interface that may be implemented by a wl_surface, for
+      implementations that provide the shell user interface.
+
+      It provides requests to set surface roles, assign an output
+      or set the position in output coordinates.
+
+      On the server side the object is automatically destroyed when
+      the related wl_surface is destroyed.  On client side,
+      org_kde_plasma_surface.destroy() must be called before
+      destroying the wl_surface object.
+    </description>
+
+    <request name="destroy" type="destructor">
+      <description summary="remove org_kde_plasma_surface interface">
+        The org_kde_plasma_surface interface is removed from the
+        wl_surface object that was turned into a shell surface with the
+        org_kde_plasma_shell.get_surface request.
+        The shell surface role is lost and wl_surface is unmapped.
+      </description>
+    </request>
+
+    <request name="set_output">
+      <description summary="assign an output to this shell surface">
+        Assign an output to this shell surface.
+        The compositor will use this information to set the position
+        when org_kde_plasma_surface.set_position request is
+        called.
+      </description>
+      <arg name="output" type="object" interface="wl_output"/>
+    </request>
+
+    <request name="set_position">
+      <description summary="change the shell surface position">
+        Move the surface to new coordinates.
+
+        Coordinates are global, for example 50,50 for a 1920,0+1920x1080 output
+        is 1970,50 in global coordinates space.
+
+        Use org_kde_plasma_surface.set_output to assign an output
+        to this surface.
+      </description>
+      <arg name="x" type="int" summary="x coordinate in global space"/>
+      <arg name="y" type="int" summary="y coordinate in global space"/>
+    </request>
+
+    <enum name="role">
+      <entry name="normal" value="0"/>
+      <entry name="desktop" value="1"/>
+      <entry name="panel" value="2"/>
+      <entry name="onscreendisplay" value="3"/>
+      <entry name="notification" value="4"/>
+      <entry name="tooltip" value="5"/>
+      <entry name="criticalnotification" value="6" since="6"/>
+      <entry name="appletpopup" value="7" since="8"/>
+    </enum>
+
+    <request name="set_role">
+      <description summary="assign a role to this surface">
+        Assign a role to a shell surface.
+
+        The compositor handles surfaces depending on their role.
+
+        This request fails if the surface already has a role, this means
+        the surface role may be assigned only once.
+
+        == Surfaces with desktop role ==
+
+        Desktop surfaces are placed below all other surfaces and are used
+        to show the actual desktop view with icons, search results or
+        controls the user will interact with. What to show depends on the
+        shell implementation.
+
+        The surfaces are placed according to the output coordinates.
+        No size is imposed to those surfaces, the shell has to resize
+        them according to output size.
+
+        Only one surface per output can have the desktop role.
+      </description>
+      <arg name="role" type="uint"/>
+    </request>
+
+    <enum name="panel_behavior">
+      <entry name="always_visible" value="1"/>
+      <entry name="auto_hide" value="2"/>
+      <entry name="windows_can_cover" value="3"/>
+      <entry name="windows_go_below" value="4"/>
+    </enum>
+
+    <request name="set_panel_behavior">
+      <description summary="set or unset the panel behavior">
+        Deprecated in Plasma 6. Has no effect.
+      </description>
+      <arg name="flag" type="uint" summary="panel_behavior enum value"/>
+    </request>
+
+    <request name="set_skip_taskbar" since="2">
+      <description summary="make the window skip the taskbar">
+        Setting this bit to the window, will make it say it prefers to not be listed in the taskbar.
+      </description>
+      <arg name="skip" type="uint" summary="Boolean value that sets whether to skip the taskbar"/>
+    </request>
+
+    <enum name="error" since="4">
+      <entry name="panel_not_auto_hide" value="0"
+             summary="Request panel_auto_hide performed on a non auto-hide panel."/>
+    </enum>
+
+    <request name="panel_auto_hide_hide" since="4">
+      <description summary="Hide the auto-hiding panel"/>
+    </request>
+
+    <request name="panel_auto_hide_show" since="4">
+      <description summary="Show the auto-hiding panel"/>
+    </request>
+
+    <request name="set_panel_takes_focus" since="4">
+      <description summary="Whether a panel takes focus">
+        By default various org_kde_plasma_surface roles do not take focus and cannot be
+        activated. With this request the compositor can be instructed to pass focus also to this
+        org_kde_plasma_surface.
+      </description>
+      <arg name="takes_focus" type="uint" summary="Boolean value that sets whether the panel takes focus"/>
+    </request>
+
+    <event name="auto_hidden_panel_hidden" since="4">
+      <description summary="Auto-hiding panel is hidden"/>
+    </event>
+
+    <event name="auto_hidden_panel_shown" since="4">
+      <description summary="Auto-hiding panel is shown"/>
+    </event>
+
+    <request name="set_skip_switcher" since="5">
+      <description summary="make the window not appear in a switcher">
+        Setting this bit will indicate that the window prefers not to be listed in a switcher.
+      </description>
+      <arg name="skip" type="uint" summary="Boolean value that sets whether to skip the window switcher."/>
+    </request>
+
+    <request name="open_under_cursor" since="7">
+      <description summary="open under cursor">
+        Request the initial position of this surface to be under the current
+        cursor position. Has to be called before attaching any buffer to this surface.
+      </description>
+    </request>
+  </interface>
+</protocol>

+ 25 - 1
src/WallpaperEngine/Application/ApplicationContext.cpp

@@ -466,7 +466,8 @@ void ApplicationContext::loadSettingsFromArgv () {
     backgroundGroup.add_argument ("--layer")
 	.help (
 	    "Wayland-only: which wlr-layer-shell layer to anchor the wallpaper to "
-	    "(background, bottom, top, overlay). Default: bottom. "
+	    "(background, bottom, top, overlay). Default: background on KDE, bottom elsewhere. "
+	    "KDE defaults to 'background' because KWin's 'show desktop' (Super+D) hides 'bottom' surfaces. "
 	    "Use 'background' on niri to pair with the `place-within-backdrop` layer-rule, "
 	    "otherwise the wallpaper will be cloned to every workspace in the overview."
 	)
@@ -657,6 +658,29 @@ void ApplicationContext::loadSettingsFromArgv () {
     try {
 	program.parse_known_args (this->m_argc, this->m_argv);
 
+	if (!program.is_used ("--layer")) {
+	    const auto* desktopSession = std::getenv ("DESKTOP_SESSION");
+	    const auto* xdgDesktop = std::getenv ("XDG_SESSION_DESKTOP");
+	    const auto* kdeSession = std::getenv ("KDE_FULL_SESSION");
+
+	    bool onKDE = false;
+	    if (desktopSession) {
+		std::string_view s (desktopSession);
+		onKDE = s.find ("kde") != std::string_view::npos || s.find ("plasma") != std::string_view::npos;
+	    }
+	    if (!onKDE && xdgDesktop && std::string_view (xdgDesktop) == "KDE") {
+		onKDE = true;
+	    }
+	    if (!onKDE && kdeSession && std::string_view (kdeSession) == "true") {
+		onKDE = true;
+	    }
+
+	    if (onKDE) {
+		this->settings.render.wayland.layer = WAYLAND_LAYER_BACKGROUND;
+		sLog.out ("KDE detected: using --layer background to prevent 'show desktop' from hiding the wallpaper (use --layer bottom to override)");
+	    }
+	}
+
 	if (this->settings.general.defaultBackground.empty ()) {
 	    throw std::runtime_error ("At least one background ID must be specified");
 	}

+ 229 - 0
src/WallpaperEngine/Render/Drivers/Detectors/KDEWaylandFullScreenDetector.cpp

@@ -0,0 +1,229 @@
+#include "KDEWaylandFullScreenDetector.h"
+
+#include "WallpaperEngine/Logging/Log.h"
+
+#include <algorithm>
+#include <cctype>
+#include <cstring>
+#include <string>
+
+namespace WallpaperEngine::Render::Drivers::Detectors {
+
+KDEWaylandFullScreenDetector::KDEWaylandFullScreenDetector (Application::ApplicationContext& appContext) :
+    FullScreenDetector (appContext) {
+    if (!initializeDBus ()) {
+	sLog.out (
+	    "KDE Wayland maximize detector could not initialize DBus. Falling back to the Wayland fullscreen detector"
+	);
+    }
+}
+
+KDEWaylandFullScreenDetector::~KDEWaylandFullScreenDetector () { stopDBus (); }
+
+bool KDEWaylandFullScreenDetector::initializeDBus () {
+    DBusError error;
+    dbus_error_init (&error);
+
+    m_connection = dbus_bus_get (DBUS_BUS_SESSION, &error);
+    if (dbus_error_is_set (&error)) {
+	sLog.error ("Failed to connect to the session DBus: ", error.message);
+	dbus_error_free (&error);
+	return false;
+    }
+
+    if (m_connection == nullptr) {
+	sLog.error ("Failed to connect to the session DBus: unknown error");
+	return false;
+    }
+
+    dbus_connection_set_exit_on_disconnect (m_connection, false);
+
+    dbus_error_init (&error);
+    const auto requestResult = dbus_bus_request_name (m_connection, kServiceName, DBUS_NAME_FLAG_DO_NOT_QUEUE, &error);
+
+    if (dbus_error_is_set (&error)) {
+	sLog.error ("Failed to request DBus service ", kServiceName, ": ", error.message);
+	dbus_error_free (&error);
+	dbus_connection_unref (m_connection);
+	m_connection = nullptr;
+	return false;
+    }
+
+    if (requestResult != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER
+	&& requestResult != DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER) {
+	sLog.error ("DBus service ", kServiceName, " is already owned by another process");
+	dbus_connection_unref (m_connection);
+	m_connection = nullptr;
+	return false;
+    }
+
+    static constexpr DBusObjectPathVTable objectVTable = {
+	.unregister_function = nullptr,
+	.message_function = &KDEWaylandFullScreenDetector::handleMessage,
+    };
+
+    if (!dbus_connection_register_object_path (m_connection, kObjectPath, &objectVTable, this)) {
+	sLog.error ("Failed to register DBus object path for KDE Wayland maximize detector");
+	dbus_connection_unref (m_connection);
+	m_connection = nullptr;
+	return false;
+    }
+
+    return true;
+}
+
+void KDEWaylandFullScreenDetector::stopDBus () {
+    if (m_connection != nullptr) {
+	dbus_connection_unregister_object_path (m_connection, kObjectPath);
+	dbus_connection_unref (m_connection);
+	m_connection = nullptr;
+    }
+}
+
+DBusHandlerResult
+KDEWaylandFullScreenDetector::handleMessage (DBusConnection* connection, DBusMessage* message, void* userData) {
+    const auto detector = static_cast<KDEWaylandFullScreenDetector*> (userData);
+    return detector->handleMessage (message);
+}
+
+DBusHandlerResult KDEWaylandFullScreenDetector::handleMessage (DBusMessage* message) {
+    if (message == nullptr) {
+	return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+    }
+
+    if (dbus_message_get_type (message) != DBUS_MESSAGE_TYPE_METHOD_CALL) {
+	return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+    }
+
+    if (const auto* currentInterface = dbus_message_get_interface (message);
+	currentInterface == nullptr || std::strcmp (currentInterface, kServiceName) != 0) {
+	return DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+    }
+
+    return handleMethodCall (message) ? DBUS_HANDLER_RESULT_HANDLED : DBUS_HANDLER_RESULT_NOT_YET_HANDLED;
+}
+
+bool KDEWaylandFullScreenDetector::handleMethodCall (DBusMessage* message) {
+    const auto* member = dbus_message_get_member (message);
+    if (member == nullptr) {
+	return false;
+    }
+
+    if (std::strcmp (member, kMethodName) != 0) {
+	return false;
+    }
+
+    DBusError error;
+    dbus_error_init (&error);
+
+    const char* windowKey = nullptr;
+    const char* windowName = nullptr;
+    const char* appId = nullptr;
+    dbus_int32_t pid = 0;
+    dbus_bool_t horizontal = false;
+    dbus_bool_t vertical = false;
+    dbus_bool_t fully = false;
+
+    if (!dbus_message_get_args (
+	    message, &error, DBUS_TYPE_STRING, &windowKey, DBUS_TYPE_STRING, &windowName, DBUS_TYPE_INT32, &pid,
+	    DBUS_TYPE_STRING, &appId, DBUS_TYPE_BOOLEAN, &horizontal, DBUS_TYPE_BOOLEAN, &vertical, DBUS_TYPE_BOOLEAN,
+	    &fully, DBUS_TYPE_INVALID
+	)) {
+	sLog.error ("Invalid KDE Wayland maximize DBus call: ", error.message);
+	dbus_error_free (&error);
+	return false;
+    }
+
+    updateWindowState (windowKey, appId, horizontal != false, vertical != false, fully != false);
+
+    if (m_connection != nullptr) {
+	DBusMessage* reply = dbus_message_new_method_return (message);
+	if (reply != nullptr) {
+	    dbus_connection_send (m_connection, reply, nullptr);
+	    dbus_message_unref (reply);
+	}
+    }
+
+    return true;
+}
+
+bool KDEWaylandFullScreenDetector::updateWindowState (
+    const std::string& windowKey, const std::string& appId, bool horizontal, bool vertical, bool fully
+) {
+    m_windowStates.insert_or_assign (
+	windowKey,
+	WindowState {
+	    .horizontal = horizontal,
+	    .vertical = vertical,
+	    .fully = fully,
+	    .appId = appId,
+	}
+    );
+
+    if (!windowKey.empty ()) {
+	m_activeWindowKey = windowKey;
+    }
+    return true;
+}
+
+bool KDEWaylandFullScreenDetector::anythingFullscreen () const {
+    if (m_connection != nullptr) {
+	if (!dbus_connection_read_write_dispatch (m_connection, 0)) {
+	    sLog.error ("DBus connection dropped unexpectedly");
+	    m_windowStates.clear ();
+	    m_activeWindowKey.clear ();
+	    dbus_connection_unregister_object_path (m_connection, kObjectPath);
+	    dbus_connection_unref (m_connection);
+	    m_connection = nullptr;
+	}
+    }
+
+    const auto& ctx = getApplicationContext ();
+
+    auto isRelevant = [&ctx] (const WindowState& state) {
+	if (!state.fully) {
+	    return false;
+	}
+
+	if (!state.appId.empty ()) {
+	    std::string appIdLower = state.appId;
+	    std::transform (appIdLower.begin (), appIdLower.end (), appIdLower.begin (), [] (unsigned char c) {
+		return std::tolower (c);
+	    });
+
+	    for (const auto& ignore : ctx.settings.render.fullscreenPauseIgnoreAppIds) {
+		if (ignore.empty ()) {
+		    continue;
+		}
+
+		std::string ignoreLower = ignore;
+		std::transform (ignoreLower.begin (), ignoreLower.end (), ignoreLower.begin (), [] (unsigned char c) {
+		    return std::tolower (c);
+		});
+
+		if (appIdLower.find (ignoreLower) != std::string::npos) {
+		    return false;
+		}
+	    }
+	}
+	return true;
+    };
+
+    if (ctx.settings.render.pauseOnFullscreenOnlyWhenActive) {
+	const auto activeIt = m_windowStates.find (m_activeWindowKey);
+	return activeIt != m_windowStates.end () && isRelevant (activeIt->second);
+    }
+
+    return std::any_of (m_windowStates.begin (), m_windowStates.end (), [&] (const auto& e) {
+	return isRelevant (e.second);
+    });
+}
+
+bool KDEWaylandFullScreenDetector::isInitialized () const { return m_connection != nullptr; }
+
+void KDEWaylandFullScreenDetector::reset () {
+    m_windowStates.clear ();
+    m_activeWindowKey.clear ();
+}
+
+} // namespace WallpaperEngine::Render::Drivers::Detectors

+ 164 - 0
src/WallpaperEngine/Render/Drivers/Detectors/KDEWaylandFullScreenDetector.h

@@ -0,0 +1,164 @@
+#pragma once
+
+#ifdef ENABLE_WAYLAND
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+
+#include <dbus/dbus.h>
+#include <string>
+#include <unordered_map>
+
+#include "FullScreenDetector.h"
+
+namespace WallpaperEngine::Render::Drivers::Detectors {
+
+/**
+ * @brief KDE Plasma/KWin fullscreen and maximize detector using D-Bus.
+ *
+ * Registers itself as a D-Bus service on the session bus so that a companion
+ * KWin script can call @c OnWindowChanged whenever a window's maximize or
+ * fullscreen state changes. Incoming notifications are stored per-window and
+ * queried by @c anythingFullscreen() to decide whether the wallpaper engine
+ * should pause rendering.
+ *
+ * If D-Bus initialization fails, @c m_connection is left as @c nullptr and
+ * the caller is expected to fall back to @c WaylandFullScreenDetector instead.
+ *
+ * @note Only compiled when @c ENABLE_WAYLAND and @c ENABLE_KDE_EXPERIMENTAL_FEATURES are defined.
+ */
+class KDEWaylandFullScreenDetector final : public FullScreenDetector {
+public:
+    explicit KDEWaylandFullScreenDetector (Application::ApplicationContext& appContext);
+
+    /**
+     * @brief Destructor. Releases the session-bus connection.
+     */
+    ~KDEWaylandFullScreenDetector () override;
+
+    /**
+     * @brief Returns @c true if any relevant window is currently fullscreen or
+     *        fully maximized.
+     *
+     * Queries the window-state map populated by @c OnWindowChanged calls from
+     * the KWin script. If no notifications have arrived (e.g. no window is
+     * fullscreen, or no state has changed since startup), returns @c false.
+     *
+     * When @c pauseOnFullscreenOnlyWhenActive is set in the render settings,
+     * only the most-recently-activated window is examined; otherwise all
+     * tracked windows are checked.
+     *
+     * App IDs listed in @c fullscreenPauseIgnoreAppIds are excluded from
+     * consideration (substring match).
+     *
+     * @return @c true if at least one non-ignored fully-maximized or
+     *         fullscreen window exists under the current pause policy.
+     */
+    [[nodiscard]] bool anythingFullscreen () const override;
+
+    [[nodiscard]] bool isInitialized () const;
+
+    /**
+     * @brief Clears all cached window states.
+     *
+     * After this call @c anythingFullscreen() will return @c false until the
+     * next @c OnWindowChanged notification arrives.
+     */
+    void reset () override;
+
+private:
+    /**
+     * @brief Snapshot of the maximize/fullscreen state for a single window.
+     */
+    struct WindowState {
+	bool horizontal = false; ///< Window spans the full horizontal extent of its output.
+	bool vertical = false; ///< Window spans the full vertical extent of its output.
+	bool fully = false; ///< Window is fully maximized or in true fullscreen mode.
+	std::string appId {}; ///< Wayland app-id / desktop file name, used for ignore-list matching.
+    };
+
+    /**
+     * @brief Static C-linkage trampoline required by the libdbus object-path vtable.
+     *
+     * Casts @p userData back to a @c KDEWaylandFullScreenDetector pointer and
+     * delegates to the non-static overload.
+     *
+     * @param connection  The D-Bus connection that received the message.
+     * @param message     The incoming D-Bus message.
+     * @param userData    Pointer to the owning @c KDEWaylandFullScreenDetector instance.
+     * @return @c DBUS_HANDLER_RESULT_HANDLED if the message was consumed,
+     *         @c DBUS_HANDLER_RESULT_NOT_YET_HANDLED otherwise.
+     */
+    static DBusHandlerResult handleMessage (DBusConnection* connection, DBusMessage* message, void* userData);
+
+    /**
+     * @brief Instance-level message handler. Filters by message type and
+     *        interface name before forwarding to @c handleMethodCall().
+     *
+     * @param message  The incoming D-Bus message.
+     * @return @c DBUS_HANDLER_RESULT_HANDLED if the message was consumed,
+     *         @c DBUS_HANDLER_RESULT_NOT_YET_HANDLED otherwise.
+     */
+    DBusHandlerResult handleMessage (DBusMessage* message);
+
+    /**
+     * @brief Connects to the session D-Bus, requests the service name, and
+     *        registers the object path.
+     *
+     * @return @c true on success; @c false if any D-Bus operation fails, in
+     *         which case the connection is released and @c m_connection is left
+     *         as @c nullptr.
+     */
+    bool initializeDBus ();
+
+    /**
+     * @brief Releases the D-Bus connection.
+     */
+    void stopDBus ();
+
+    /**
+     * @brief Parses and handles an @c OnWindowChanged method-call message.
+     *
+     * Expected arguments:
+     *   - @c STRING  windowKey
+     *   - @c STRING  windowName
+     *   - @c INT32   pid
+     *   - @c STRING  appId
+     *   - @c BOOLEAN horizontal
+     *   - @c BOOLEAN vertical
+     *   - @c BOOLEAN fully
+     *
+     * @param message  A D-Bus method-call message.
+     * @return @c true if the message was successfully parsed and handled.
+     */
+    bool handleMethodCall (DBusMessage* message);
+
+    /**
+     * @brief Inserts or replaces the @c WindowState entry for @p windowKey
+     *        and updates the active-window key.
+     *
+     * @param windowKey   Opaque string identifier for the window.
+     * @param appId       Wayland app-id used for ignore-list matching.
+     * @param horizontal  Whether the window occupies the full screen width.
+     * @param vertical    Whether the window occupies the full screen height.
+     * @param fully       Whether the window is fully maximized or fullscreen.
+     */
+    bool updateWindowState (
+	const std::string& windowKey, const std::string& appId, bool horizontal, bool vertical, bool fully
+    );
+
+    /** @brief Session D-Bus connection handle; @c nullptr if initialization failed. */
+    mutable DBusConnection* m_connection = nullptr;
+
+    /** @brief Per-window maximize/fullscreen state, keyed by the window's opaque identifier. */
+    mutable std::unordered_map<std::string, WindowState> m_windowStates;
+
+    /** @brief Key of the most recently activated (focused) window. */
+    mutable std::string m_activeWindowKey;
+
+    static constexpr const char* kServiceName = "org.linuxwallpaperengine.WaylandDetector";
+    static constexpr const char* kObjectPath = "/org/linuxwallpaperengine/WaylandDetector";
+    static constexpr const char* kMethodName = "OnWindowChanged";
+};
+
+} // namespace WallpaperEngine::Render::Drivers::Detectors
+#endif /* ENABLE_KDE_EXPERIMENTAL_FEATURES */
+#endif /* ENABLE_WAYLAND */

+ 38 - 0
src/WallpaperEngine/Render/Drivers/Detectors/WaylandFullScreenDetector.cpp

@@ -1,8 +1,13 @@
 #include "WaylandFullScreenDetector.h"
 
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#include "KDEWaylandFullScreenDetector.h"
+#endif
+
 #include "WallpaperEngine/Logging/Log.h"
 #include "WallpaperEngine/Render/Drivers/VideoFactories.h"
 #include "wlr-foreign-toplevel-management-unstable-v1-protocol.h"
+#include <cstdlib>
 #include <cstring>
 #include <string>
 #include <string_view>
@@ -85,6 +90,30 @@ namespace {
 	}
     }
 
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+    bool isRunningUnderKDE () {
+	const char* desktopSession = std::getenv ("DESKTOP_SESSION");
+	if (desktopSession != nullptr) {
+	    std::string_view session (desktopSession);
+	    if (session.find ("kde") != std::string_view::npos || session.find ("plasma") != std::string_view::npos) {
+		return true;
+	    }
+	}
+
+	const char* xdgDesktop = std::getenv ("XDG_SESSION_DESKTOP");
+	if (xdgDesktop != nullptr && std::string_view (xdgDesktop) == "KDE") {
+	    return true;
+	}
+
+	const char* kdeSession = std::getenv ("KDE_FULL_SESSION");
+	if (kdeSession != nullptr && std::string (kdeSession) == "true") {
+	    return true;
+	}
+
+	return false;
+    }
+#endif
+
     bool isRelevant (
 	const WallpaperEngine::Application::ApplicationContext& ctx, const bool fullscreen, const bool activated,
 	const std::string& appId
@@ -258,6 +287,15 @@ void WaylandFullScreenDetector::reset () { }
 __attribute__ ((constructor)) void registerWaylandFullscreenDetector () {
     sVideoFactories.registerFullscreenDetector (
 	"wayland", [] (ApplicationContext& context, VideoDriver& driver) -> std::unique_ptr<FullScreenDetector> {
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+	    if (isRunningUnderKDE ()) {
+		auto detector = std::make_unique<KDEWaylandFullScreenDetector> (context);
+		if (!detector->isInitialized ()) {
+		    return std::make_unique<WaylandFullScreenDetector> (context);
+		}
+		return detector;
+	    }
+#endif
 	    return std::make_unique<WaylandFullScreenDetector> (context);
 	}
     );

+ 16 - 1
src/WallpaperEngine/Render/Drivers/Output/WaylandOutputViewport.cpp

@@ -8,6 +8,9 @@ extern "C" {
 #include "wlr-layer-shell-unstable-v1-protocol.h"
 #include "xdg-output-unstable-v1-protocol.h"
 #include "xdg-shell-protocol.h"
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#include "plasma-shell-protocol.h"
+#endif
 }
 #undef class
 #undef namespace
@@ -182,7 +185,7 @@ void WaylandOutputViewport::setupLS () {
 
     layerSurface = zwlr_layer_shell_v1_get_layer_surface (
 	m_driver->getWaylandContext ()->layerShell, surface, output, wlrLayer,
-	"linux-wallpaperengine"
+	"desktop"
     );
 
     if (!layerSurface) {
@@ -213,6 +216,18 @@ void WaylandOutputViewport::setupLS () {
     zwlr_layer_surface_v1_set_exclusive_zone (layerSurface, -1);
     wl_surface_set_input_region (surface, region);
     wl_region_destroy (region);
+
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+    if (m_driver->getWaylandContext ()->plasmaShell) {
+	plasmaSurface = org_kde_plasma_shell_get_surface (m_driver->getWaylandContext ()->plasmaShell, surface);
+	if (plasmaSurface) {
+	    org_kde_plasma_surface_set_role (plasmaSurface, ORG_KDE_PLASMA_SURFACE_ROLE_DESKTOP);
+	    org_kde_plasma_surface_set_skip_taskbar (plasmaSurface, 1);
+	    org_kde_plasma_surface_set_skip_switcher (plasmaSurface, 1);
+	}
+    }
+#endif
+
     wl_surface_commit (surface);
     wl_display_roundtrip (m_driver->getWaylandContext ()->display);
 

+ 6 - 0
src/WallpaperEngine/Render/Drivers/Output/WaylandOutputViewport.h

@@ -18,6 +18,9 @@ struct zwlr_layer_shell_v1;
 struct zwlr_layer_surface_v1;
 struct zxdg_output_v1;
 struct zxdg_output_manager_v1;
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+struct org_kde_plasma_surface;
+#endif
 
 namespace WallpaperEngine::Render::Drivers {
 class WaylandOpenGLDriver;
@@ -47,6 +50,9 @@ namespace Output {
 	wl_surface* surface = nullptr;
 	zwlr_layer_surface_v1* layerSurface = nullptr;
 	wl_callback* frameCallback = nullptr;
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+	org_kde_plasma_surface* plasmaSurface = nullptr;
+#endif
 	glm::dvec2 mousePos = { 0, 0 };
 	WallpaperEngine::Input::MouseClickStatus leftClick = WallpaperEngine::Input::MouseClickStatus::Released;
 	WallpaperEngine::Input::MouseClickStatus rightClick = WallpaperEngine::Input::MouseClickStatus::Released;

+ 24 - 4
src/WallpaperEngine/Render/Drivers/WaylandOpenGLDriver.cpp

@@ -10,6 +10,9 @@ extern "C" {
 #include "wlr-layer-shell-unstable-v1-protocol.h"
 #include "xdg-output-unstable-v1-protocol.h"
 #include "xdg-shell-protocol.h"
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+#include "plasma-shell-protocol.h"
+#endif
 #include <linux/input-event-codes.h>
 }
 #undef class
@@ -125,6 +128,12 @@ handleGlobal (void* data, struct wl_registry* registry, uint32_t name, const cha
 	driver->getWaylandContext ()->xdgOutputManager = static_cast<zxdg_output_manager_v1*> (
 	    wl_registry_bind (registry, name, &zxdg_output_manager_v1_interface, std::min (version, 3u))
 	);
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+    } else if (strcmp (interface, org_kde_plasma_shell_interface.name) == 0) {
+	driver->getWaylandContext ()->plasmaShell = static_cast<org_kde_plasma_shell*> (
+	    wl_registry_bind (registry, name, &org_kde_plasma_shell_interface, std::min (version, 8u))
+	);
+#endif
     }
 }
 
@@ -246,6 +255,13 @@ void WaylandOpenGLDriver::onLayerClose (Output::WaylandOutputViewport* viewport)
 	zwlr_layer_surface_v1_destroy (viewport->layerSurface);
     }
 
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+    if (viewport->plasmaSurface) {
+	org_kde_plasma_surface_destroy (viewport->plasmaSurface);
+	viewport->plasmaSurface = nullptr;
+    }
+#endif
+
     if (viewport->xdgOutput) {
 	zxdg_output_v1_destroy (viewport->xdgOutput);
 	viewport->xdgOutput = nullptr;
@@ -359,11 +375,15 @@ void WaylandOpenGLDriver::setupOutputLayerSurfaces () {
 void WaylandOpenGLDriver::initGLEW () {
     glewExperimental = GL_TRUE;
     if (const GLenum result = glewInit (); result != GLEW_OK) {
-	if (result == GLEW_ERROR_NO_GLX_DISPLAY) {
-	    sLog.out ("Failed to initialize GLEW, but continuing with EGL context: No GLX display");
+	const char* error = reinterpret_cast<const char*> (glewGetErrorString (result));
+	// On Wayland+EGL, GLEW may fail with GLEW_ERROR_NO_GLX_DISPLAY or an unrecognised
+	// code (null string) when the build also includes X11 but no GLX display is present.
+	// Both are non-fatal: the EGL context is already current and GLEW extensions still load.
+	if (result == GLEW_ERROR_NO_GLX_DISPLAY || error == nullptr) {
+	    sLog.out ("Failed to initialize GLEW, but continuing with EGL context: ",
+		      error ? error : "No GLX display");
 	} else {
-	    const char* error = reinterpret_cast<const char*> (glewGetErrorString (result));
-	    sLog.error ("Failed to initialize GLEW: ", error ? error : "Unknown error");
+	    sLog.error ("Failed to initialize GLEW: ", error);
 	    sLog.exception ("Cannot continue...");
 	}
     }

+ 6 - 0
src/WallpaperEngine/Render/Drivers/WaylandOpenGLDriver.h

@@ -25,6 +25,9 @@ class WallpaperApplication;
 struct zwlr_layer_shell_v1;
 struct zwlr_layer_surface_v1;
 struct zxdg_output_manager_v1;
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+struct org_kde_plasma_shell;
+#endif
 
 namespace WallpaperEngine::Render::Drivers {
 using namespace WallpaperEngine::Application;
@@ -56,6 +59,9 @@ public:
 	zwlr_layer_shell_v1* layerShell = nullptr;
 	wl_seat* seat = nullptr;
 	zxdg_output_manager_v1* xdgOutputManager = nullptr;
+#ifdef ENABLE_KDE_EXPERIMENTAL_FEATURES
+	org_kde_plasma_shell* plasmaShell = nullptr;
+#endif
     };
 
     explicit WaylandOpenGLDriver (ApplicationContext& context, WallpaperApplication& app);

+ 2 - 0
src/WallpaperEngine/Render/Shaders/GLSLContext.cpp

@@ -178,6 +178,7 @@ std::pair<std::string, std::string> GLSLContext::toGlsl (const std::string& vert
     spirv_cross::CompilerGLSL::Options options;
     options.version = 330;
     options.es = false;
+    options.force_zero_initialized_variables = true;
     vertexCompiler.set_common_options (options);
 
     spirv.clear ();
@@ -186,6 +187,7 @@ std::pair<std::string, std::string> GLSLContext::toGlsl (const std::string& vert
     spirv_cross::CompilerGLSL fragmentCompiler (spirv);
     options.version = 330;
     options.es = false;
+    options.force_zero_initialized_variables = true;
     fragmentCompiler.set_common_options (options);
 
     return { vertexCompiler.compile () + "#if 0\n" + vertex + "\n#endif",