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.10master
parent
d830fbbe16
commit
b19b3596c1
|
@ -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',
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -70,7 +70,7 @@ ipython =
|
|||
ipykernel
|
||||
jupyter_console
|
||||
wayland =
|
||||
pywlroots>=0.14.8
|
||||
pywlroots>=0.14.10
|
||||
xkbcommon>=0.3
|
||||
|
||||
[options.package_data]
|
||||
|
|
4
tox.ini
4
tox.ini
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue