Repository: qtile/qtile

Issue:
Moving a fullscreened window to a different group makes it disappear from all groups
When I use `lazy.window.togroup(group_name)` to send a window to another group and the window is fullscreened, it does not appear on the target group.

Some logs:
```
2022-02-10 22:33:15,589 ERROR libqtile manager.py:process_key_event():L391 KB command error togroup: Traceback (most recent call last):
  File "/home/mcol/git/qtile/libqtile/command/interface.py", line 312, in call
    return SUCCESS, cmd(*args, **kwargs)
  File "/home/mcol/git/qtile/libqtile/backend/base.py", line 422, in cmd_togroup
    self.togroup(group_name, switch_group=switch_group)
  File "/home/mcol/git/qtile/libqtile/backend/wayland/window.py", line 370, in togroup
    self.group.remove(self)
  File "/home/mcol/git/qtile/libqtile/group.py", line 295, in remove
    self.tiled_windows.remove(win)
KeyError: Window(name='imv - [1/1] [1920x1080] [100%] /home/mcol/20220210_22h32m45s_grim.png [shrink to fit] ', wid=13)
```

This suggests that, as the window is removed from the current group but then the action errors out, the fullscreened window just remains in an unmapped state.

I think this is a relatively new bug. Pinging @jwijenbergh - I suspect it's to do with your recent fullscreen changes.


Discussion:
I cannot reproduce this (x11 through xephyr). I also tested this when I made the pr. Does this happen for all of your windows?
I tested it like this:

- Start xephyr
- Fullscreen terminal
- Move terminal to workspace 2

Ooh, it only happens with windows that were never tiled, i.e. have matches in `float_rules`. Perhaps it's enough to check they are in that `self.tiled_windows` list before removing them.
> Ooh, it only happens with windows that were never tiled, i.e. have matches in `float_rules`. Perhaps it's enough to check they are in that `self.tiled_windows` list before removing them.

Can confirm. We need to indeed add `if win in self.tiled_windows` when removing it. I think I removed this check when simplifying the pr because I thought it wasn't need any more
Maybe this is also related to the bug by @mithicspirit

Edit: I mean part of the issue of #3308 
I will look into it. It seems to be a different issue, maybe I can make a pr for both issues during the weekend?
Progress already made in #3324. Haven't got the chance to test a lot so lmk if you see any issues @m-col 

Resolving patch:
diff --git a/libqtile/backend/wayland/window.py b/libqtile/backend/wayland/window.py
--- a/libqtile/backend/wayland/window.py
+++ b/libqtile/backend/wayland/window.py
@@ -423,6 +423,7 @@ def floating(self, do_float):
                 # if we are setting floating early, e.g. from a hook, we don't have a screen yet
                 self._float_state = FloatStates.FLOATING
         elif (not do_float) and self._float_state != FloatStates.NOT_FLOATING:
+            self.update_fullscreen(False)
             if self._float_state == FloatStates.FLOATING:
                 # store last size
                 self._float_width = self.width
@@ -431,13 +432,21 @@ def floating(self, do_float):
             self.group.mark_floating(self, False)
             hook.fire("float_change")
 
+    def update_fullscreen(self, do_full):
+        # already done updating previously
+        if do_full == self.fullscreen:
+            return
+
+        # update surface fullscreen and ftm
+        self.surface.set_fullscreen(do_full)
+        self.ftm_handle.set_fullscreen(do_full)
+
     @property
     def fullscreen(self):
         return self._float_state == FloatStates.FULLSCREEN
 
     @fullscreen.setter
     def fullscreen(self, do_full):
-        self.surface.set_fullscreen(do_full)
         if do_full:
             screen = self.group.screen or self.qtile.find_closest_screen(self.x, self.y)
             bw = self.group.floating_layout.fullscreen_border_width
@@ -451,8 +460,6 @@ def fullscreen(self, do_full):
         elif self._float_state == FloatStates.FULLSCREEN:
             self.floating = False
 
-        self.ftm_handle.set_fullscreen(do_full)
-
     @property
     def maximized(self):
         return self._float_state == FloatStates.MAXIMIZED
@@ -594,6 +601,7 @@ def _enablefloating(
         self._reconfigure_floating(x, y, w, h, new_float_state)
 
     def _reconfigure_floating(self, x, y, w, h, new_float_state=FloatStates.FLOATING):
+        self.update_fullscreen(new_float_state == FloatStates.FULLSCREEN)
         if new_float_state == FloatStates.MINIMIZED:
             self.hide()
         else:
diff --git a/libqtile/backend/x11/window.py b/libqtile/backend/x11/window.py
--- a/libqtile/backend/x11/window.py
+++ b/libqtile/backend/x11/window.py
@@ -1305,6 +1305,7 @@ def floating(self, do_float):
                 # if we are setting floating early, e.g. from a hook, we don't have a screen yet
                 self._float_state = FloatStates.FLOATING
         elif (not do_float) and self._float_state != FloatStates.NOT_FLOATING:
+            self.update_fullscreen_wm_state(False)
             if self._float_state == FloatStates.FLOATING:
                 # store last size
                 self._float_width = self.width
@@ -1324,19 +1325,30 @@ def wants_to_fullscreen(self):
     def toggle_floating(self):
         self.floating = not self.floating
 
+    def set_wm_state(self, old_state, new_state):
+        if new_state != old_state:
+            self.window.set_property("_NET_WM_STATE", list(new_state))
+
+    def update_fullscreen_wm_state(self, do_full):
+        # already done updating previously
+        if do_full == self.fullscreen:
+            return
+
+        # update fullscreen _NET_WM_STATE
+        atom = set([self.qtile.core.conn.atoms["_NET_WM_STATE_FULLSCREEN"]])
+        prev_state = set(self.window.get_property("_NET_WM_STATE", "ATOM", unpack=int))
+
+        if do_full:
+            self.set_wm_state(prev_state, prev_state | atom)
+        else:
+            self.set_wm_state(prev_state, prev_state - atom)
+
     @property
     def fullscreen(self):
         return self._float_state == FloatStates.FULLSCREEN
 
     @fullscreen.setter
     def fullscreen(self, do_full):
-        atom = set([self.qtile.core.conn.atoms["_NET_WM_STATE_FULLSCREEN"]])
-        prev_state = set(self.window.get_property("_NET_WM_STATE", "ATOM", unpack=int))
-
-        def set_state(old_state, new_state):
-            if new_state != old_state:
-                self.window.set_property("_NET_WM_STATE", list(new_state))
-
         if do_full:
             screen = self.group.screen or self.qtile.find_closest_screen(self.x, self.y)
 
@@ -1348,13 +1360,9 @@ def set_state(old_state, new_state):
                 screen.height - 2 * bw,
                 new_float_state=FloatStates.FULLSCREEN,
             )
-            set_state(prev_state, prev_state | atom)
             return
 
         if self._float_state == FloatStates.FULLSCREEN:
-            # The order of calling set_state() and then
-            # setting self.floating = False is important
-            set_state(prev_state, prev_state - atom)
             self.floating = False
             return
 
@@ -1467,6 +1475,7 @@ def getposition(self):
         return (self.x, self.y)
 
     def _reconfigure_floating(self, new_float_state=FloatStates.FLOATING):
+        self.update_fullscreen_wm_state(new_float_state == FloatStates.FULLSCREEN)
         if new_float_state == FloatStates.MINIMIZED:
             self.state = IconicState
             self.hide()
diff --git a/libqtile/group.py b/libqtile/group.py
--- a/libqtile/group.py
+++ b/libqtile/group.py
@@ -252,9 +252,9 @@ def add(self, win, focus=True, force=False):
         win.group = self
         if self.qtile.config.auto_fullscreen and win.wants_to_fullscreen:
             win._float_state = FloatStates.FULLSCREEN
-        elif self.floating_layout.match(win):
+        elif self.floating_layout.match(win) and not win.fullscreen:
             win._float_state = FloatStates.FLOATING
-        if win.floating:
+        if win.floating and not win.fullscreen:
             self.floating_layout.add(win)
         if not win.floating or win.fullscreen:
             self.tiled_windows.add(win)
@@ -292,7 +292,8 @@ def remove(self, win, force=False):
                 or self.layout.focus_first()
             )
 
-            self.tiled_windows.remove(win)
+            if win in self.tiled_windows:
+                self.tiled_windows.remove(win)
 
         # a notification may not have focus
         if hadfocus:
@@ -317,9 +318,9 @@ def mark_floating(self, win, floating):
                         i.remove(win)
                         if win is self.current_window:
                             i.blur()
-                self.floating_layout.add(win)
-                if win is self.current_window:
-                    self.floating_layout.focus(win)
+                    self.floating_layout.add(win)
+                    if win is self.current_window:
+                        self.floating_layout.focus(win)
         else:
             self.floating_layout.remove(win)
             self.floating_layout.blur()


Test patch: