WL: support foreign toplevel management protocol

This adds supports for the wlr_foreign_toplevel_management_v1 protocol.
This provides an interface through which clients such as status bars and
other WM utilities can control windows belonging to regular clients.
This provides similar functionality as seen in X via e.g. xdotool,
wmctrl to manipulate windows in some ways.

Requires pywlroots 0.14.10
master
mcol 2021-09-28 00:29:34 +01:00 committed by elParaguayo
parent d830fbbe16
commit b19b3596c1
5 changed files with 106 additions and 18 deletions

View File

@ -55,6 +55,7 @@ MOCK_MODULES = [
'wlroots.util.region',
'wlroots.wlr_types',
'wlroots.wlr_types.cursor',
'wlroots.wlr_types.foreign_toplevel_management_v1',
'wlroots.wlr_types.keyboard',
'wlroots.wlr_types.layer_shell_v1',
'wlroots.wlr_types.output_management_v1',

View File

@ -33,6 +33,7 @@ from wlroots.wlr_types import (
Cursor,
DataControlManagerV1,
DataDeviceManager,
ForeignToplevelManagerV1,
GammaControlManagerV1,
OutputLayout,
PrimarySelectionV1DeviceManager,
@ -191,6 +192,7 @@ class Core(base.Core, wlrq.HasListeners):
self.pointer_constraints: Set[wlrq.PointerConstraint] = set()
self.active_pointer_constraint: Optional[wlrq.PointerConstraint] = None
self._relative_pointer_manager_v1 = RelativePointerManagerV1(self.display)
self.foreign_toplevel_manager_v1 = ForeignToplevelManagerV1.create(self.display)
# start
os.environ["WAYLAND_DISPLAY"] = self.socket.decode()
@ -513,7 +515,6 @@ class Core(base.Core, wlrq.HasListeners):
and self.qtile.current_screen != win.group.screen
):
self.qtile.focus_screen(win.group.screen.index, False)
self.focus_window(win, surface)
if self._hovered_internal:
self._hovered_internal = None
@ -643,6 +644,10 @@ class Core(base.Core, wlrq.HasListeners):
if self.focused_internal:
self.focused_internal = None
if isinstance(win.surface, LayerSurfaceV1):
if not win.surface.current.keyboard_interactive:
return
previous_surface = self.seat.keyboard_state.focused_surface
if previous_surface == surface:
return
@ -652,18 +657,17 @@ class Core(base.Core, wlrq.HasListeners):
previous_xdg_surface = XdgSurface.from_surface(previous_surface)
if not win or win.surface != previous_xdg_surface:
previous_xdg_surface.set_activated(False)
if previous_xdg_surface.data:
previous_xdg_surface.data.set_activated(False)
if not win:
self.seat.keyboard_clear_focus()
return
if isinstance(win.surface, LayerSurfaceV1):
if not win.surface.current.keyboard_interactive:
return
logger.debug("Focussing new window")
if surface.is_xdg_surface and isinstance(win.surface, XdgSurface):
win.surface.set_activated(True)
win.ftm_handle.set_activated(True)
if enter and self.seat.keyboard._ptr: # This pointer is NULL when headless
self.seat.keyboard_notify_enter(surface, self.seat.keyboard)
@ -682,13 +686,15 @@ class Core(base.Core, wlrq.HasListeners):
win.cmd_bring_to_front()
if not isinstance(win, base.Internal):
if not isinstance(win, base.Static):
if isinstance(win, window.Static):
if win.screen is not self.qtile.current_screen:
self.qtile.focus_screen(win.screen.index, warp=False)
win.focus(False)
else:
if win.group and win.group.screen is not self.qtile.current_screen:
self.qtile.focus_screen(win.group.screen.index, warp=False)
self.qtile.current_group.focus(win, False)
self.focus_window(win, surface=surface, enter=False)
else:
screen = self.qtile.find_screen(self.cursor.x, self.cursor.y)
if screen:

View File

@ -25,6 +25,7 @@ import typing
import cairocffi
import pywayland
import wlroots.wlr_types.foreign_toplevel_management_v1 as ftm
from wlroots import ffi
from wlroots.util.box import Box
from wlroots.util.edges import Edges
@ -94,6 +95,8 @@ class Window(base.Window, HasListeners):
assert isinstance(surface, XdgSurface)
self._app_id: Optional[str] = surface.toplevel.app_id
self.ftm_handle = core.foreign_toplevel_manager_v1.create_handle()
surface.data = self.ftm_handle
self._float_state = FloatStates.NOT_FLOATING
self.float_x: Optional[int] = None
@ -117,6 +120,8 @@ class Window(base.Window, HasListeners):
if pc.window is self:
pc.finalize()
self.ftm_handle.destroy()
@property
def wid(self):
return self._wid
@ -181,11 +186,29 @@ class Window(base.Window, HasListeners):
# Get the client's name
if surface.toplevel.title:
self.name = surface.toplevel.title
self.ftm_handle.set_title(self.name)
if self._app_id:
self.ftm_handle.set_app_id(self._app_id)
# Add the toplevel's listeners
self.add_listener(surface.toplevel.request_fullscreen_event, self._on_request_fullscreen)
self.add_listener(surface.toplevel.set_title_event, self._on_set_title)
self.add_listener(surface.toplevel.set_app_id_event, self._on_set_app_id)
self.add_listener(
self.ftm_handle.request_maximize_event, self._on_foreign_request_maximize
)
self.add_listener(
self.ftm_handle.request_minimize_event, self._on_foreign_request_minimize
)
self.add_listener(
self.ftm_handle.request_activate_event, self._on_foreign_request_activate
)
self.add_listener(
self.ftm_handle.request_fullscreen_event, self._on_foreign_request_fullscreen
)
self.add_listener(
self.ftm_handle.request_close_event, self._on_foreign_request_close
)
self.qtile.manage(self)
@ -226,11 +249,13 @@ class Window(base.Window, HasListeners):
def _on_set_title(self, _listener, _data):
logger.debug("Signal: window set_title")
self.name = self.surface.toplevel.title
self.ftm_handle.set_title(self.name)
hook.fire('client_name_updated', self)
def _on_set_app_id(self, _listener, _data):
logger.debug("Signal: window set_app_id")
self._app_id = self.surface.toplevel.app_id
self.ftm_handle.set_app_id(self._app_id)
def _on_commit(self, _listener, _data):
self.damage()
@ -238,6 +263,36 @@ class Window(base.Window, HasListeners):
def _on_new_subsurface(self, _listener, subsurface: WlrSubSurface):
self.subsurfaces.append(SubSurface(self, subsurface))
def _on_foreign_request_maximize(
self, _listener, event: ftm.ForeignToplevelHandleV1MaximizedEvent
):
logger.debug("Signal: foreign_toplevel_management request_maximize")
self.maximized = event.maximized
def _on_foreign_request_minimize(
self, _listener, event: ftm.ForeignToplevelHandleV1MinimizedEvent
):
logger.debug("Signal: foreign_toplevel_management request_minimize")
self.minimized = event.minimized
def _on_foreign_request_fullscreen(
self, _listener, event: ftm.ForeignToplevelHandleV1FullscreenEvent
):
logger.debug("Signal: foreign_toplevel_management request_fullscreen")
self.fullscreen = event.fullscreen
def _on_foreign_request_activate(
self, _listener, event: ftm.ForeignToplevelHandleV1ActivatedEvent
):
logger.debug("Signal: foreign_toplevel_management request_activate")
if self.group:
self.qtile.current_screen.set_group(self.group)
self.group.focus(self)
def _on_foreign_request_close(self, _listener, _event):
logger.debug("Signal: foreign_toplevel_management request_close")
self.kill()
def has_fixed_size(self) -> bool:
assert isinstance(self.surface, XdgSurface)
state = self.surface.toplevel._ptr.current
@ -371,11 +426,11 @@ class Window(base.Window, HasListeners):
screen.height - 2 * bw,
new_float_state=FloatStates.FULLSCREEN
)
return
if self._float_state == FloatStates.FULLSCREEN:
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
@ -398,6 +453,8 @@ class Window(base.Window, HasListeners):
if self._float_state == FloatStates.MAXIMIZED:
self.floating = False
self.ftm_handle.set_maximized(do_maximize)
@property
def minimized(self):
return self._float_state == FloatStates.MINIMIZED
@ -411,11 +468,10 @@ class Window(base.Window, HasListeners):
if self._float_state == FloatStates.MINIMIZED:
self.floating = False
self.ftm_handle.set_minimized(do_minimize)
def focus(self, warp: bool) -> None:
self.core.focus_window(self)
if isinstance(self, base.Internal):
# self.core.focus_window is enough for internal windows
return
if warp and self.qtile.config.cursor_warp:
self.core.warp_pointer(
@ -425,6 +481,7 @@ class Window(base.Window, HasListeners):
if self.group:
self.group.current_window = self
hook.fire("client_focus", self)
def place(self, x, y, width, height, borderwidth, bordercolor,
@ -756,6 +813,9 @@ class Internal(base.Internal, Window):
self.mapped = True
self.damage()
def focus(self, warp: bool) -> None:
self.core.focus_window(self)
def kill(self) -> None:
self.hide()
if self.wid in self.qtile.windows_map:
@ -843,6 +903,16 @@ class Static(base.Static, Window):
self.add_listener(surface.toplevel.set_title_event, self._on_set_title)
self.add_listener(surface.toplevel.set_app_id_event, self._on_set_app_id)
self._find_outputs()
self.screen = qtile.current_screen
def finalize(self):
self.finalize_listeners()
for subsurface in self.subsurfaces:
subsurface.finalize()
for pc in self.core.pointer_constraints.copy():
if pc.window is self:
pc.finalize()
@property
def mapped(self) -> bool:
@ -880,7 +950,7 @@ class Static(base.Static, Window):
self.mapped = True
if self.is_layer:
self.output.organise_layers()
self.core.focus_window(self, self.surface.surface)
self.focus(True)
def _on_unmap(self, _listener, data):
logger.debug("Signal: window unmap")
@ -898,6 +968,17 @@ class Static(base.Static, Window):
def has_fixed_size(self) -> bool:
return False
def focus(self, warp: bool) -> None:
self.core.focus_window(self)
if warp and self.qtile.config.cursor_warp:
self.core.warp_pointer(
self.x + self.width // 2,
self.y + self.height // 2,
)
hook.fire("client_focus", self)
def kill(self):
if self.is_layer:
self.surface.close()

View File

@ -70,7 +70,7 @@ ipython =
ipykernel
jupyter_console
wayland =
pywlroots>=0.14.8
pywlroots>=0.14.10
xkbcommon>=0.3
[options.package_data]

View File

@ -35,7 +35,7 @@ deps =
PyGObject
# pywayland has to be installed before pywlroots
commands =
pip install pywlroots>=0.14.8
pip install pywlroots>=0.14.10
python3 setup.py -q install
{toxinidir}/scripts/ffibuild
python3 -m pytest -W error --cov libqtile --cov-report term-missing --backend=x11 --backend=wayland {posargs}
@ -89,7 +89,7 @@ deps =
types-pkg_resources
commands =
pip install -r requirements.txt pywayland>=0.4.4 xkbcommon>=0.3
pip install pywlroots>=0.14.8
pip install pywlroots>=0.14.10
mypy -p libqtile
# also run the tests that require mypy
python3 setup.py -q install