Search code examples
pythonubuntupyqt5gnomexlib

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


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)

Solution

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