Repository: qtile/qtile

Issue:
Popups and child-windows are drawn behind parent windows in certain situations
# Issue description
In some situations I noticed that popups or child-windows are drawn (or moved) behind their respective parents; for child-windows this can be quite a problem in case the child is blocking, since then the software might be completely unresponsive.

Places where I have noticed this and that I remember:
1. Virtualbox (fullscreen) (the main window, not a client): Context menus on VM-list open in the background.
2. Veracrypt (floating) "mount" child-window: The window either spawns in the back or is moved there once it loses focus.

There have been some other cases, but due to testing PR #1870, which I initially expected to be the cause for this, I haven't taken note of all of them.
Some software definitely has working popup-windows, so Virtualbox might be doing something weird.

# Qtile version

https://github.com/qtile/qtile/commit/c6b80e9444e9b4e5dc2f52ca1954dc502c45f2eb
(newer versions not tested yet)

# Configuration

I'll only include one snippet here, since verything else (setting up Keys and Widgets) most likely has nothing to do with the problem.

```python
@hook.subscribe.client_new
def modify_window(client):
    if (client.window.get_wm_transient_for() or client.window.get_wm_type() in floating_types):
        client.floating = True
```

On top of this I am currently using fake screens instead of normal screens.

It would be nice if somebody could confirm this, since I am not entirely sure this isn't all caused by my own PR.

Related to #1870, which could fix at least the described issue with child-windows.


Discussion:
Yes, I've noticed this with the Gimp export floating windows also: when you export as PNG, the second floating window (PNG settings) appears behind the first one.
In Steam too, in floating layout
Not sure if it's related or not, but if you click a button on that Gimp floating window for PNG settings and there's the other floating window behind, then the button won't get clicked and the other window will get focus. Really weird.
Yes, that also happens to me on Steam, in the chat pop-up window
But, for example in OBS Studio floating windows do not present problems
I also have a few programs that seem to work fine. Steam is a bit weird, though - especially the menus don't open reliably for me.

If you aren't running the git master, please include the version you are running. I don't remember any of this behavior in 0.15.1, so it might have been introduced since then.
qtile --version
0.16.2.dev26+g10645392
Might be related to PR #1710 (commit https://github.com/qtile/qtile/commit/0f886606e1f35226b325f5405d43946976eae831 ).

Resolving patch:
diff --git a/libqtile/layout/floating.py b/libqtile/layout/floating.py
--- a/libqtile/layout/floating.py
+++ b/libqtile/layout/floating.py
@@ -179,35 +179,53 @@ def focus(self, client):
     def blur(self):
         self.focused = None
 
+    def on_screen(self, client, screen_rect):
+        if client.x < screen_rect.x:  # client's left edge
+            return False
+        if screen_rect.x + screen_rect.width < client.x + client.width:  # right
+            return False
+        if client.y < screen_rect.y:  # top
+            return False
+        if screen_rect.y + screen_rect.width < client.y + client.height:  # bottom
+            return False
+        return True
+
     def compute_client_position(self, client, screen_rect):
         """ recompute client.x and client.y, returning whether or not to place
         this client above other windows or not """
-        above = False
-        transient_for = client.window.get_wm_transient_for()
-        win = client.group.qtile.windows_map.get(transient_for)
-        if win is not None:
-            # if transient for a window, place in the center of the window
-            center_x = win.x + win.width / 2
-            center_y = win.y + win.height / 2
-        else:
-            center_x = screen_rect.x + screen_rect.width / 2
-            center_y = screen_rect.y + screen_rect.height / 2
-            above = True
-
-        x = center_x - client.width / 2
-        y = center_y - client.height / 2
-
-        # don't go off the right...
-        x = min(x, screen_rect.x + screen_rect.width)
-        # or left...
-        x = max(x, screen_rect.x)
-        # or bottom...
-        y = min(y, screen_rect.y + screen_rect.height)
-        # or top
-        y = max(y, screen_rect.y)
-
-        client.x = int(round(x))
-        client.y = int(round(y))
+        above = True
+
+        if client.has_user_set_position() and not self.on_screen(client, screen_rect):
+            # move to screen
+            client.x = screen_rect.x + client.x
+            client.y = screen_rect.y + client.y
+        if not client.has_user_set_position() or not self.on_screen(client, screen_rect):
+            # client has not been properly placed before or it is off screen
+            transient_for = client.window.get_wm_transient_for()
+            win = client.group.qtile.windows_map.get(transient_for)
+            if win is not None:
+                # if transient for a window, place in the center of the window
+                center_x = win.x + win.width / 2
+                center_y = win.y + win.height / 2
+                above = False
+            else:
+                center_x = screen_rect.x + screen_rect.width / 2
+                center_y = screen_rect.y + screen_rect.height / 2
+
+            x = center_x - client.width / 2
+            y = center_y - client.height / 2
+
+            # don't go off the right...
+            x = min(x, screen_rect.x + screen_rect.width - client.width)
+            # or left...
+            x = max(x, screen_rect.x)
+            # or bottom...
+            y = min(y, screen_rect.y + screen_rect.height - client.height)
+            # or top
+            y = max(y, screen_rect.y)
+
+            client.x = int(round(x))
+            client.y = int(round(y))
         return above
 
     def configure(self, client, screen_rect):
@@ -231,14 +249,7 @@ def configure(self, client, screen_rect):
             client.paint_borders(bc, bw)
             client.cmd_bring_to_front()
 
-        # similar to above but the X11 version, the client may have already
-        # placed itself. let's respect that
-        elif client.has_user_set_position():
-            client.paint_borders(bc, bw)
-            client.cmd_bring_to_front()
-
-        # ok, it's not java and the window itself didn't position it, but users
-        # may still have asked us not to mess with it
+        # alternatively, users may have asked us explicitly to leave the client alone
         elif any(m.compare(client) for m in self.no_reposition_rules):
             client.paint_borders(bc, bw)
             client.cmd_bring_to_front()


Test patch: