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