Repository: qtile/qtile

Issue:
[Enhancement] Execute Keybindings while holding mouse buttons down
# Issue description
Imagine this situation. I have groups 1 containing a browser window and 2 containing a file manager. I want to drag a file from the file manager to the browser across windows.  Or even on the same group switching between windows with alt+tab. Considering all those groups have the Max layout.

Currently that is not possible because it seems that holding Button1. Button2 or Button3 down blocks any shortcut I have set on my config. It would be ideal to be able to execute those shortcuts or at least enable ignoring the mouse buttons for them, while holding the mouse button. Then I guess that would be enough to drag files across groups or different windows in max layout ?

# Qtile version

``` 
$ qtile --version
0.17.1.dev315+g67f97604

```
OS: Arch Linux x86_64


Discussion:
I came up with a little very hacky workaround for now. Maybe someone knows a better way.

qtile_delay_toggle_.py
```python
#!/usr/bin/python3
from libqtile.command.client import InteractiveCommandClient
import subprocess
import time
from sys import argv

COUNTDOWN_TIME = 2

c = InteractiveCommandClient()

def notify(title="notification", body="", time=None):
    if time is None:
        subprocess.Popen(["notify-send", f"{str(title)}", f"{str(body)}"])
    else:
        subprocess.Popen(
            ["notify-send", "-t", f"{time}", f"{str(title)}", f"{str(body)}"]
        )

def file_drag_hack(on_group=False):
    for i in range(COUNTDOWN_TIME):
        notify(f"Switching in {COUNTDOWN-i}", '', 1)
        time.sleep(1.3)
    if on_group:
        c.screen.togglegroup()
    else:
        c.layout.down()


if __name__ == "__main__":
    file_drag_hack(argv[1] if len(argv) > 1 else False)
```

~/config/qtile/config.py
```python

def shell(cmd):
    subprocess.Popen(cmd, shell=True)

def file_drag_hack(qtile, on_group=False):
    cmd = f"{home}/bin/qtile_delay_toggle {'1' if on_group else ''}"
    shell(cmd)

keys += [
    KeyChord(
        [mod],
        "space",
        [
            Key([mod], "Tab", lazy.function(lambda q: file_drag_hack(q, True))),
            Key([alt], "Tab", lazy.function(file_drag_hack)),
    ]
]
```

This will add a delayed action for toggling window or group.

I could not add the toggling function to the config.py file itself because
seems like mouse click and drag events are blocked while qtile is processing
lazy callbacks.
This may be on x11 only. Key presses are passed successfully on wayland when holding a mouse button. I'm not clear if this is a limitation of X. If so, we'll probably just close this.
When I hold the left mouse button, the `event.state` of the key press event handled by `Core.handle_KeyPress` (x11) is different, by what appears to be a modifier of value 0x100. This is consistent with xev, which shows "state 0x100" when I click. This indicates that the left mouse button acts as a modifier, changing which key bindings are being matched and executed. I suspect we may be able to remove all mouse-related modifiers from the bindings when we get key presses. In fact, this does just that:

```diff
diff --git a/libqtile/backend/x11/core.py b/libqtile/backend/x11/core.py
index a18c53dc..84bb8808 100644
--- a/libqtile/backend/x11/core.py
+++ b/libqtile/backend/x11/core.py
@@ -162,7 +162,7 @@ class Core(base.Core):
 
         numlock_code = self.conn.keysym_to_keycode(xcbq.keysyms["Num_Lock"])[0]
         self._numlock_mask = xcbq.ModMasks.get(self.conn.get_modifier(numlock_code), 0)
-        self._valid_mask = ~(self._numlock_mask | xcbq.ModMasks["lock"])
+        self._valid_mask = ~(self._numlock_mask | xcbq.ModMasks["lock"] | xcbq.AllButtonsMask)
 
     @property
     def name(self):

```

No idea what side effects this may have... probably worth extensive testing :D
If this causes problems with mouse button bindings then this path instead only removes button modifiers from KeyPress events:
```diff
diff --git a/libqtile/backend/x11/core.py b/libqtile/backend/x11/core.py
index 84bb8808..192f06fe 100644
--- a/libqtile/backend/x11/core.py
+++ b/libqtile/backend/x11/core.py
@@ -605,7 +605,7 @@ class Core(base.Core):
         assert self.qtile is not None
 
         keysym = self.conn.code_to_syms[event.detail][0]
-        self.qtile.process_key_event(keysym, event.state & self._valid_mask)
+        self.qtile.process_key_event(keysym, event.state & self._valid_mask & ~xcbq.AllButtonsMask)
 
     def handle_ButtonPress(self, event) -> None:  # noqa: N802
         assert self.qtile is not None

```
> When I hold the left mouse button, the `event.state` of the key press event handled by `Core.handle_KeyPress` (x11) is different, by what appears to be a modifier of value 0x100. This is consistent with xev, which shows "state 0x100" when I click. This indicates that the left mouse button acts as a modifier, changing which key bindings are being matched and executed. I suspect we may be able to remove all mouse-related modifiers from the bindings when we get key presses. In fact, this does just that:
> 
> ```diff
> diff --git a/libqtile/backend/x11/core.py b/libqtile/backend/x11/core.py
> index a18c53dc..84bb8808 100644
> --- a/libqtile/backend/x11/core.py
> +++ b/libqtile/backend/x11/core.py
> @@ -162,7 +162,7 @@ class Core(base.Core):
>  
>          numlock_code = self.conn.keysym_to_keycode(xcbq.keysyms["Num_Lock"])[0]
>          self._numlock_mask = xcbq.ModMasks.get(self.conn.get_modifier(numlock_code), 0)
> -        self._valid_mask = ~(self._numlock_mask | xcbq.ModMasks["lock"])
> +        self._valid_mask = ~(self._numlock_mask | xcbq.ModMasks["lock"] | xcbq.AllButtonsMask)
>  
>      @property
>      def name(self):
> ```
> 
> No idea what side effects this may have... probably worth extensive testing :D

This patch worked fine until now! And i can drag files between groups with this! I have no problems with mouse bindings that are defined inside the `mouse ` list on the config with this one at all. My bindings for resize and move floating windows still work on this patch. Example:
```python
mouse = [
    Drag(
        [mod],
        "Button1",
        lazy.window.set_position_floating(),
        start=lazy.window.get_position(),
    ),
    Drag(
        [mod], "Button3", lazy.window.set_size_floating(), start=lazy.window.get_size()
    ),
    Click([mod], "Button2", lazy.window.bring_to_front()),
]
```

Are all still working ok!


> If this causes problems with mouse button bindings then this path instead only removes button modifiers from KeyPress events:
> 
> ```diff
> diff --git a/libqtile/backend/x11/core.py b/libqtile/backend/x11/core.py
> index 84bb8808..192f06fe 100644
> --- a/libqtile/backend/x11/core.py
> +++ b/libqtile/backend/x11/core.py
> @@ -605,7 +605,7 @@ class Core(base.Core):
>          assert self.qtile is not None
>  
>          keysym = self.conn.code_to_syms[event.detail][0]
> -        self.qtile.process_key_event(keysym, event.state & self._valid_mask)
> +        self.qtile.process_key_event(keysym, event.state & self._valid_mask & ~xcbq.AllButtonsMask)
>  
>      def handle_ButtonPress(self, event) -> None:  # noqa: N802
>          assert self.qtile is not None
> ```

So I'm not sure if that is what you mean by mouse bindings. This patch alone didn't let me do shortcuts while holding the mouse buttons down so this doesn't work I suppose.
OK thanks for the update. That was what I meant by mouse bindings, so that helps. I'll submit the first form as a PR and hopefully some others can test it out for a few days so we can be extra certain this causes no issues.

Resolving patch:
diff --git a/libqtile/backend/x11/core.py b/libqtile/backend/x11/core.py
--- a/libqtile/backend/x11/core.py
+++ b/libqtile/backend/x11/core.py
@@ -161,7 +161,7 @@ def __init__(self, display_name: str = None) -> None:
 
         numlock_code = self.conn.keysym_to_keycode(xcbq.keysyms["Num_Lock"])[0]
         self._numlock_mask = xcbq.ModMasks.get(self.conn.get_modifier(numlock_code), 0)
-        self._valid_mask = ~(self._numlock_mask | xcbq.ModMasks["lock"])
+        self._valid_mask = ~(self._numlock_mask | xcbq.ModMasks["lock"] | xcbq.AllButtonsMask)
 
     @property
     def name(self):
@@ -602,9 +602,7 @@ def handle_ButtonPress(self, event) -> None:  # noqa: N802
         assert self.qtile is not None
 
         button_code = event.detail
-        state = event.state
-        state |= self._numlock_mask
-        state &= self._valid_mask
+        state = event.state & self._valid_mask
 
         if not event.child:  # The client's handle_ButtonPress will focus it
             self.focus_by_click(event)
@@ -618,8 +616,7 @@ def handle_ButtonRelease(self, event) -> None:  # noqa: N802
         assert self.qtile is not None
 
         button_code = event.detail
-        state = event.state | self._numlock_mask
-        state &= self._valid_mask & ~xcbq.AllButtonsMask
+        state = event.state & self._valid_mask
         self.qtile.process_button_release(button_code, state)
 
     def handle_MotionNotify(self, event) -> None:  # noqa: N802


Test patch: