Skip to content

Commit

Permalink
x11: Add z-layer stacking
Browse files Browse the repository at this point in the history
This commit implements z layer stacking for the x11 backend.

See the discussion at qtile#3409 for more
details about the commit.

Window stacking is implemented by providing windows with two key
functions: `get_layering_information` and `change_layer`.

`get_layering_information` returns details about which layer the window
should be placed. This is based on the WM spec
(https://specifications.freedesktop.org/wm-spec/1.3/ar01s07.html#STACKINGORDER)
but with some additions for qtile (e.g. scratchpads are placed at the
top).

`change_layer` is the method that places the window based on these
rules.

Windows can be moved up and down the stack and pinned to certain layers.

Floating windows should be placed above tiled windows by default.

There will undoubtedly be some bugs but we should merge this for now so
we can get it out into hands of more users.
  • Loading branch information
elParaguayo committed Jul 21, 2023
1 parent ba7efd0 commit 50cbcb1
Show file tree
Hide file tree
Showing 16 changed files with 854 additions and 84 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ Qtile x.xx.x, released XXXX-XX-XX:
- Add ability to set custom "Undefined" status key value to `Mpd2Widget`.
- `Mpd2Widget` now searches for artist name in all similar keys (i.e `albumartist`, `performer`, etc.).
- Add svg support to `CustomLayoutIcon`
- added layering controls for X11 (Wayland support coming soon!):
- `lazy.window.keep_above()/keep_below()` marks windows to be kept above/below other windows permanently.
Calling the functions with no arguments toggles the state, otherwise pass `enable=True` or `enable=False`.
- `lazy.window.move_up()/move_down()` moves windows up and down the z axis.
- added `only_focused` setting to Max layout, allowing to draw multiple clients on top of each other when
set to False
* bugfixes
- Fix bug where Window.center() centers window on the wrong screen when using multiple monitors.
- Fix `Notify` bug when apps close notifications.
Expand Down
3 changes: 3 additions & 0 deletions docs/manual/config/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,9 @@ configuration variables that control specific aspects of Qtile's behavior:
custom floating rules among other things if you wish.

See the configuration file for the default `float_rules`.
* - ``floats_kept_above``
- ``True``
- Floating windows are kept above tiled windows (Currently x11 only. Wayland support coming soon.)
* - ``focus_on_window_activation``
- ``'smart'``
- Behavior of the _NET_ACTIVATE_WINDOW message sent by applications
Expand Down
17 changes: 17 additions & 0 deletions docs/manual/config/lazy.rst
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,23 @@ Window functions
- Put the focused window to/from floating mode
* - ``lazy.window.toggle_fullscreen()``
- Put the focused window to/from fullscreen mode
* - ``lazy.window.move_up()``
- Move the window above the next window in the stack.
* - ``lazy.window.move_down()``
- Move the window below the previous window in the stack.
* - ``lazy.window.move_to_top()``
- Move the window above all other windows with similar priority
(i.e. a "normal" window will not be moved above a ``kept_above`` window).
* - ``lazy.window.move_to_bottom()``
- Move the window below all other windows with similar priority
(i.e. a "normal" window will not be moved below a ``kept_below`` window).
* - ``lazy.window.keep_above()``
- Keep window above other windows.
* - ``lazy.window.keep_below()``
- Keep window below other windows.
* - ``lazy.window.bring_to_front()``
- Bring window above all other windows. Ignores ``kept_above`` priority.


Screen functions
----------------
Expand Down
57 changes: 54 additions & 3 deletions libqtile/backend/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,6 @@ def on_config_load(self, initial: bool) -> None:
def warp_pointer(self, x: int, y: int) -> None:
"""Warp the pointer to the given coordinates relative."""

def update_client_list(self, windows_map: dict[int, WindowType]) -> None:
"""Update the list of windows being managed"""

@contextlib.contextmanager
def masked(self):
"""A context manager to suppress window events while operating on many windows."""
Expand Down Expand Up @@ -253,6 +250,60 @@ def info(self) -> dict[str, Any]:
"""
return {}

@expose_command()
def keep_above(self, enable: bool | None = None):
"""Keep this window above all others"""

@expose_command()
def keep_below(self, enable: bool | None = None):
"""Keep this window below all others"""

@expose_command()
def move_up(self, force: bool = False) -> None:
"""
Move this window above the next window along the z axis.
Will not raise a "normal" window (i.e. one that is not "kept_above/below")
above a window that is marked as "kept_above".
Will not raise a window where "keep_below" is True unless
force is set to True.
"""

@expose_command()
def move_down(self, force: bool = False) -> None:
"""
Move this window below the previous window along the z axis.
Will not lower a "normal" window (i.e. one that is not "kept_above/below")
below a window that is marked as "kept_below".
Will not lower a window where "keep_above" is True unless
force is set to True.
"""

@expose_command()
def move_to_top(self) -> None:
"""
Move this window above all windows in the current layer
e.g. if you have 3 windows all with "keep_above" set, calling
this method will move the window to the top of those three windows.
Calling this on a "normal" window will not raise it above a "kept_above"
window.
"""

@expose_command()
def move_to_bottom(self) -> None:
"""
Move this window below all windows in the current layer
e.g. if you have 3 windows all with "keep_above" set, calling
this method will move the window to the bottom of those three windows.
Calling this on a "normal" window will not raise it below a "kept_below"
window.
"""


class Window(_Window, metaclass=ABCMeta):
"""
Expand Down
53 changes: 42 additions & 11 deletions libqtile/backend/x11/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import xcffib.render
import xcffib.xproto
import xcffib.xtest
from xcffib.xproto import EventMask, StackMode
from xcffib.xproto import EventMask

from libqtile import config, hook, utils
from libqtile.backend import base
Expand Down Expand Up @@ -167,6 +167,8 @@ def __init__(self, display_name: str | None = None) -> None:
# The last motion notify event that we still need to handle
self._motion_notify: xcffib.Event | None = None

self.last_focused: base.Window | None = None

@property
def name(self):
return "x11"
Expand Down Expand Up @@ -232,6 +234,9 @@ def on_config_load(self, initial) -> None:
"""Assign windows to groups"""
assert self.qtile is not None

# Ensure that properties are initialised at startup
self.update_client_lists()

if not initial:
# We are just reloading config
for win in self.qtile.windows_map.values():
Expand All @@ -240,8 +245,8 @@ def on_config_load(self, initial) -> None:
return

# Qtile just started - scan for clients
_, _, children = self._root.query_tree()
for item in children:
for wid in self._root.query_tree():
item = window.XWindow(self.conn, wid)
try:
attrs = item.get_attributes()
state = item.get_wm_state()
Expand Down Expand Up @@ -276,6 +281,9 @@ def on_config_load(self, initial) -> None:

self.qtile.manage(win)

self.update_client_lists()
win.change_layer()

def warp_pointer(self, x, y):
self._root.warp_pointer(x, y)
self._root.set_input_focus()
Expand Down Expand Up @@ -441,17 +449,28 @@ def display_name(self) -> str:
"""The name of the connected display"""
return self._display_name

def update_client_list(self, windows_map: dict[int, base.WindowType]) -> None:
"""Updates the client stack list
def update_client_lists(self) -> None:
"""Updates the _NET_CLIENT_LIST and _NET_CLIENT_LIST_STACKING properties
This is needed for third party tasklists and drag and drop of tabs in
chrome
"""
assert self.qtile
# Regular top-level managed windows, i.e. excluding Static, Internal and Systray Icons
wids = [wid for wid, c in windows_map.items() if isinstance(c, window.Window)]
wids = [wid for wid, c in self.qtile.windows_map.items() if isinstance(c, window.Window)]
self._root.set_property("_NET_CLIENT_LIST", wids)
# TODO: check stack order
self._root.set_property("_NET_CLIENT_LIST_STACKING", wids)

# We rely on the stacking order from the X server
stacked_wids = []
for wid in self._root.query_tree():
win = self.qtile.windows_map.get(wid)
if not win:
continue

if isinstance(win, window.Window) and win.group:
stacked_wids.append(wid)

self._root.set_property("_NET_CLIENT_LIST_STACKING", stacked_wids)

def update_desktops(self, groups, index: int) -> None:
"""Set the current desktops of the window manager
Expand Down Expand Up @@ -738,11 +757,14 @@ def handle_MapRequest(self, event) -> None: # noqa: N802
if not win.group or not win.group.screen:
return
win.unhide()
self.update_client_lists()
win.change_layer()

def handle_DestroyNotify(self, event) -> None: # noqa: N802
assert self.qtile is not None

self.qtile.unmanage(event.window)
self.update_client_lists()
if self.qtile.current_window is None:
self.conn.fixup_focus()

Expand All @@ -769,6 +791,7 @@ def handle_UnmapNotify(self, event) -> None: # noqa: N802
win.wid, win.window.conn.atoms["_NET_WM_DESKTOP"]
)
self.qtile.unmanage(event.window)
self.update_client_lists()
if self.qtile.current_window is None:
self.conn.fixup_focus()

Expand Down Expand Up @@ -837,9 +860,7 @@ def focus_by_click(self, e, window=None):
qtile.config.bring_front_click != "floating_only"
or getattr(window, "floating", False)
):
self.conn.conn.core.ConfigureWindow(
window.wid, xcffib.xproto.ConfigWindow.StackMode, [StackMode.Above]
)
window.bring_to_front()

try:
if window.group.screen is not qtile.current_screen:
Expand Down Expand Up @@ -908,3 +929,13 @@ def get_mouse_position(self) -> tuple[int, int]:
def keysym_from_name(self, name: str) -> int:
"""Get the keysym for a key from its name"""
return keysyms[name.lower()]

def check_stacking(self, win: base.Window) -> None:
"""Triggers restacking if a fullscreen window loses focus."""
if win is self.last_focused:
return

if self.last_focused and self.last_focused.fullscreen:
self.last_focused.change_layer()

self.last_focused = win
Loading

0 comments on commit 50cbcb1

Please sign in to comment.