2

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)
18
  • So you're saying you can place a window between the wallpaper and the desktop icons in windows? Commented Feb 26, 2022 at 22:27
  • Exactly. On windows, macOs and cinnamon, but not in GNOME. Check this if you are interested: github.com/Kalmat/PyWinCtl Commented Feb 27, 2022 at 9:37
  • On my Ubuntu, this immediately closes whichever window I have in focus; you might have warned us :-). Also, you forgot to import os. Commented Mar 3, 2022 at 16:14
  • @Kalma That is so cool! Thanks for the link :) Commented Mar 3, 2022 at 16:37
  • So, just for clarification, you want your window to look something like this, where it's behind the icons on the Desktop, but in front of the wallpaper? (that image is edited to demonstrate; unfortunately I didn't get your code to work). Commented Mar 3, 2022 at 16:43

1 Answer 1

2

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)
Sign up to request clarification or add additional context in comments.

2 Comments

That's great! Were you waiting for the update all along?
Yes! It's a great relief! And no, I didn't know this was gonna be solved in this new update, it's been a good surprise. I will upload a new module version to PyPi. I'm just waiting for testing... a lot, because new version will have some other relevant changes (a very klind github user has been working on a typed version). Thank you so much for your support @AnnZen!

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.