Place a Window behind desktop icons using PyQt on Ubuntu/GNOME

Question:

I’m trying to develop a simple cross-platform Wallpaper manager, but I am not able to find any method to place my PyQt Window between the current wallpaper and the desktop icons using XLib (on windows and macOS it’s way easier and works perfectly).

This works right on Cinnamon (with a little workround just simulating a click), but not on GNOME. Can anyone help or give me any clue? (I’m providing all this code just to provide a minimum executable piece, but the key part, I guess, is right after ‘if "GNOME"…’ sentence)

import os
import time

import Xlib
import ewmh
import pywinctl
from pynput import mouse

DISP = Xlib.display.Display()
SCREEN = DISP.screen()
ROOT = DISP.screen().root
EWMH = ewmh.EWMH(_display=DISP, root=ROOT)


def sendBehind(hWnd):

        w = DISP.create_resource_object('window', hWnd)
        w.change_property(DISP.intern_atom('_NET_WM_STATE', False), Xlib.Xatom.ATOM, 32, [DISP.intern_atom('_NET_WM_STATE_BELOW', False), ], Xlib.X.PropModeReplace)
        w.change_property(DISP.intern_atom('_NET_WM_STATE', False), Xlib.Xatom.ATOM, 32, [DISP.intern_atom('_NET_WM_STATE_SKIP_TASKBAR', False), ], Xlib.X.PropModeAppend)
        w.change_property(DISP.intern_atom('_NET_WM_STATE', False), Xlib.Xatom.ATOM, 32, [DISP.intern_atom('_NET_WM_STATE_SKIP_PAGER', False), ], Xlib.X.PropModeAppend)
        DISP.flush()

        # This sends window below all others, but not behind the desktop icons
        w.change_property(DISP.intern_atom('_NET_WM_WINDOW_TYPE', False), Xlib.Xatom.ATOM, 32, [DISP.intern_atom('_NET_WM_WINDOW_TYPE_DESKTOP', False), ],Xlib.X.PropModeReplace)
        DISP.flush()

        if "GNOME" in os.environ.get('XDG_CURRENT_DESKTOP', ""):
            # This sends the window "too far behind" (below all others, including Wallpaper, like unmapped)
            # Trying to figure out how to raise it on top of wallpaper but behind desktop icons
            desktop = _xlibGetAllWindows(title="gnome-shell")
            if desktop:
                w.reparent(desktop[-1], 0, 0)
                DISP.flush()
        else:
            # Mint/Cinnamon: just clicking on the desktop, it raises, sending the window/wallpaper to the bottom!
            m = mouse.Controller()
            m.move(SCREEN.width_in_pixels - 1, 100)
            m.click(mouse.Button.left, 1)

        return '_NET_WM_WINDOW_TYPE_DESKTOP' in EWMH.getWmWindowType(hWnd, str=True)


def bringBack(hWnd, parent):
    w = DISP.create_resource_object('window', hWnd)

    if parent:
        w.reparent(parent, 0, 0)
        DISP.flush()

    w.unmap()
    w.change_property(DISP.intern_atom('_NET_WM_WINDOW_TYPE', False), Xlib.Xatom.ATOM,
                      32, [DISP.intern_atom('_NET_WM_WINDOW_TYPE_NORMAL', False), ],
                      Xlib.X.PropModeReplace)
    DISP.flush()
    w.change_property(DISP.intern_atom('_NET_WM_STATE', False), Xlib.Xatom.ATOM,
                      32, [DISP.intern_atom('_NET_WM_STATE_FOCUSED', False), ],
                      Xlib.X.PropModeReplace)
    DISP.flush()
    w.map()
    EWMH.setActiveWindow(hWnd)
    EWMH.display.flush()
    return '_NET_WM_WINDOW_TYPE_NORMAL' in EWMH.getWmWindowType(hWnd, str=True)


def _xlibGetAllWindows(parent: int = None, title: str = ""):

    if not parent:
        parent = ROOT
    allWindows = [parent]

    def findit(hwnd):
        query = hwnd.query_tree()
        for child in query.children:
            allWindows.append(child)
            findit(child)

    findit(parent)
    if not title:
        matches = allWindows
    else:
        matches = []
        for w in allWindows:
            if w.get_wm_name() == title:
                matches.append(w)
    return matches


hWnd = pywinctl.getActiveWindow()
parent = hWnd._hWnd.query_tree().parent
sendBehind(hWnd._hWnd)
time.sleep(3)
bringBack(hWnd._hWnd, parent)
Asked By: Kalma

||

Answers:

Eureka!!! Last Ubuntu version (22.04) seems to have brought the solution by itself. It now has a "layer" for desktop icons you can interact with. This also gave me the clue to find a smarter solution on Mint/Cinnamon (testing in other OS is still pending). This is the code which seems to work OK, for those with the same issue:

import time

import Xlib.display
import ewmh
import pywinctl

DISP = Xlib.display.Display()
SCREEN = DISP.screen()
ROOT = DISP.screen().root
EWMH = ewmh.EWMH(_display=DISP, root=ROOT)


def sendBehind(hWnd):
    w = DISP.create_resource_object('window', hWnd.id)
    w.unmap()
    w.change_property(DISP.intern_atom('_NET_WM_WINDOW_TYPE', False), Xlib.Xatom.ATOM,
                      32, [DISP.intern_atom('_NET_WM_WINDOW_TYPE_DESKTOP', False), ],
                      Xlib.X.PropModeReplace)
    DISP.flush()
    w.map()

    # This will try to raise the desktop icons layer on top of the window
    # Ubuntu: "@!0,0;BDHF" is the new desktop icons NG extension
    # Mint: "Desktop" name is language-dependent. Using its class (nemo-desktop)
    desktop = _xlibGetAllWindows(title="@!0,0;BDHF", klass=('nemo-desktop', 'Nemo-desktop'))
    for d in desktop:
        w = DISP.create_resource_object('window', d)
        w.raise_window()

    return '_NET_WM_WINDOW_TYPE_DESKTOP' in EWMH.getWmWindowType(hWnd, str=True)


def bringBack(hWnd):

    w = DISP.create_resource_object('window', hWnd.id)

    w.unmap()
    w.change_property(DISP.intern_atom('_NET_WM_WINDOW_TYPE', False), Xlib.Xatom.ATOM,
                      32, [DISP.intern_atom('_NET_WM_WINDOW_TYPE_NORMAL', False), ],
                      Xlib.X.PropModeReplace)
    DISP.flush()
    w.change_property(DISP.intern_atom('_NET_WM_STATE', False), Xlib.Xatom.ATOM,
                      32, [DISP.intern_atom('_NET_WM_STATE_FOCUSED', False), ],
                      Xlib.X.PropModeReplace)
    DISP.flush()
    w.map()
    EWMH.setActiveWindow(hWnd)
    EWMH.display.flush()
    return '_NET_WM_WINDOW_TYPE_NORMAL' in EWMH.getWmWindowType(hWnd, str=True)


def _xlibGetAllWindows(parent=None, title: str = "", klass=None):

    parent = parent or ROOT
    allWindows = [parent]

    def findit(hwnd):
        query = hwnd.query_tree()
        for child in query.children:
            allWindows.append(child)
            findit(child)

    findit(parent)
    if not title and not klass:
        return allWindows
    else:
        return [window for window in allWindows if ((title and window.get_wm_name() == title) or
                                                    (klass and window.get_wm_class() == klass))]


hWnd = pywinctl.getActiveWindow()
sendBehind(hWnd._hWnd)
time.sleep(3)
bringBack(hWnd._hWnd)
Answered By: Kalma
Categories: questions Tags: , , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.