Skip to content

Commit 86a09c4

Browse files
authored
Move X11 dodge monitor behind visibility service (#144)
* Move X11 dodge monitor behind visibility service * Clarification
1 parent feff80d commit 86a09c4

12 files changed

Lines changed: 238 additions & 51 deletions

File tree

docking/app.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ def main() -> None:
108108
theme=theme,
109109
window_tracker=backend.windows,
110110
preview_service=backend.previews,
111+
visibility_service=backend.visibility,
111112
launcher=launcher,
112113
)
113114
items_service = DockItemsService(model=model, window=window)

docking/platform/backends/x11/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
build_x11_session_backend,
1919
build_x11_window_tracker,
2020
)
21+
from docking.platform.backends.x11.visibility import X11VisibilityService
2122
from docking.platform.backends.x11.windows import X11WindowService
2223

2324
__all__ = [
2425
"X11SessionBackend",
26+
"X11VisibilityService",
2527
"X11WindowService",
2628
"build_x11_session_backend",
2729
"build_x11_window_tracker",

docking/platform/backends/x11/session.py

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
from __future__ import annotations
1717

1818
import os
19-
from collections.abc import Callable
2019
from typing import TYPE_CHECKING
2120

2221
from docking.log import get_logger
@@ -26,9 +25,9 @@
2625
PlatformCapabilities,
2726
Rect,
2827
ReservationRequest,
29-
VisibilityMonitor,
3028
)
3129
from docking.platform.backends.x11.previews import X11PreviewService
30+
from docking.platform.backends.x11.visibility import X11VisibilityService
3231
from docking.platform.backends.x11.windows import X11WindowService
3332
from docking.platform.window_tracker import WindowTracker
3433

@@ -80,9 +79,9 @@ class X11SessionBackend:
8079
"""SessionBackend implementation for the current X11 runtime.
8180
8281
This backend intentionally remains X11-only. It groups the already-migrated
83-
window and preview services so application startup can depend on a session
84-
backend shape before native Wayland services exist. Surface and visibility
85-
stay transitional until their dedicated migration PRs.
82+
window, preview, and visibility services so application startup can depend
83+
on a session backend shape before native Wayland services exist. Surface
84+
stays transitional until its dedicated migration PR.
8685
"""
8786

8887
def __init__(
@@ -93,7 +92,7 @@ def __init__(
9392
)
9493
self._previews = X11PreviewService(window_tracker=self._windows)
9594
self._surface = _TransitionalSurfaceService()
96-
self._visibility = _TransitionalVisibilityService()
95+
self._visibility = X11VisibilityService(config=config)
9796

9897
@property
9998
def name(self) -> str:
@@ -147,7 +146,7 @@ def surface(self) -> _TransitionalSurfaceService:
147146
return self._surface
148147

149148
@property
150-
def visibility(self) -> _TransitionalVisibilityService:
149+
def visibility(self) -> X11VisibilityService:
151150
return self._visibility
152151

153152
@property
@@ -222,24 +221,3 @@ def update_input_region(self, rect: Rect) -> None:
222221

223222
def set_blur_region(self, rect: Rect | None) -> None:
224223
return
225-
226-
227-
class _TransitionalVisibilityService:
228-
"""Unsupported visibility service until dodge ownership moves in PR 8."""
229-
230-
def start(self) -> None:
231-
return
232-
233-
def stop(self) -> None:
234-
return
235-
236-
def supports_hide_mode(self, mode: object) -> bool:
237-
return False
238-
239-
def create_monitor(
240-
self,
241-
*,
242-
get_dock_rect: Callable[[], Rect | None],
243-
on_change: Callable[[bool], None],
244-
) -> VisibilityMonitor | None:
245-
return None
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Author: Eduardo Mucelli Rezende Oliveira
2+
# E-mail: edumucelli@gmail.com
3+
#
4+
# This program is free software: you can redistribute it and/or modify
5+
# it under the terms of the GNU General Public License as published by
6+
# the Free Software Foundation, either version 3 of the License, or
7+
# (at your option) any later version.
8+
#
9+
# This program is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
# GNU General Public License for more details.
13+
14+
"""X11 overlap/dodge visibility service."""
15+
16+
from __future__ import annotations
17+
18+
from collections.abc import Callable
19+
from typing import TYPE_CHECKING
20+
21+
from docking.core.config import HideMode
22+
from docking.platform.backends.base import Rect, VisibilityMonitor
23+
from docking.platform.dodge import ScreenRect, WindowDodgeMonitor
24+
25+
if TYPE_CHECKING:
26+
from docking.core.config import Config
27+
28+
# These are only the hide modes that depend on foreign-window overlap state.
29+
# The simpler modes such as NONE, ALWAYS_ON_TOP, and AUTOHIDE are handled by
30+
# dock autohide/surface behavior and do not need an X11 dodge monitor.
31+
_OVERLAP_HIDE_MODES = frozenset(
32+
{
33+
HideMode.INTELLIGENT,
34+
HideMode.DODGE_ACTIVE,
35+
HideMode.WINDOW_DODGE,
36+
HideMode.DODGE_MAXIMIZED,
37+
}
38+
)
39+
40+
41+
class X11VisibilityService:
42+
"""VisibilityService backed by the existing Wnck WindowDodgeMonitor."""
43+
44+
def __init__(self, *, config: Config | None) -> None:
45+
self._config = config
46+
47+
def start(self) -> None:
48+
"""No service-level runtime loop is needed."""
49+
50+
def stop(self) -> None:
51+
"""No service-level resources are held."""
52+
53+
def supports_hide_mode(self, mode: object) -> bool:
54+
"""Return whether X11 can monitor foreign-window overlap for a mode."""
55+
return mode in _OVERLAP_HIDE_MODES
56+
57+
def create_monitor(
58+
self,
59+
*,
60+
get_dock_rect: Callable[[], Rect | None],
61+
on_change: Callable[[bool], None],
62+
) -> VisibilityMonitor | None:
63+
"""Create the current X11 dodge monitor, preserving old factory wiring."""
64+
if self._config is None:
65+
return None
66+
67+
return WindowDodgeMonitor(
68+
config=self._config,
69+
get_dock_rect=lambda: _to_screen_rect(get_dock_rect()),
70+
on_change=on_change,
71+
)
72+
73+
74+
def _to_screen_rect(rect: Rect | None) -> ScreenRect | None:
75+
if rect is None:
76+
return None
77+
return ScreenRect(x=rect.x, y=rect.y, width=rect.width, height=rect.height)

docking/ui/dock_window.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@
245245
from docking.core.config import Config
246246
from docking.core.items import DockItem
247247
from docking.core.theme import Theme
248-
from docking.platform.dodge import WindowDodgeMonitor
248+
from docking.platform.backends.base import VisibilityMonitor
249249
from docking.platform.launcher import Launcher
250250
from docking.platform.model import DockModel
251251
from docking.platform.window_tracker import WindowTracker
@@ -379,7 +379,7 @@ def __init__(
379379
self._redraw_source_id: int | None = None
380380
self.dock_hovered: bool = False
381381
self._last_autohide_state: HideState | None = None
382-
self.dodge_monitor: WindowDodgeMonitor | None = None
382+
self.dodge_monitor: VisibilityMonitor | None = None
383383

384384
self._setup_window()
385385
self._setup_drawing_area()

docking/ui/factory.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222

2323
from docking.core.config import Config
2424
from docking.core.theme import Theme
25-
from docking.platform.backends.base import PreviewService
26-
from docking.platform.dodge import ScreenRect, WindowDodgeMonitor
25+
from docking.platform.backends.base import PreviewService, Rect, VisibilityService
2726
from docking.platform.launcher import Launcher
2827
from docking.platform.model import DockModel
2928
from docking.platform.window_tracker import WindowTracker
@@ -39,6 +38,7 @@ def build_dock_window(
3938
theme: Theme,
4039
window_tracker: WindowTracker,
4140
preview_service: PreviewService,
41+
visibility_service: VisibilityService,
4242
launcher: Launcher,
4343
) -> DockWindow:
4444
"""Build a fully wired dock window and its UI collaborators."""
@@ -52,18 +52,18 @@ def build_dock_window(
5252
preview_service=preview_service,
5353
)
5454

55-
def _get_dock_rect() -> ScreenRect | None:
55+
def _get_dock_rect() -> Rect | None:
5656
if not window.get_realized():
5757
return None
5858
wx, wy = window.get_position()
5959
ww, wh = window.get_size()
60-
return ScreenRect(x=wx, y=wy, width=ww, height=wh)
60+
return Rect(x=wx, y=wy, width=ww, height=wh)
6161

62-
dodge_monitor = WindowDodgeMonitor(
63-
config=config,
62+
dodge_monitor = visibility_service.create_monitor(
6463
get_dock_rect=_get_dock_rect,
6564
on_change=window.autohide.set_window_should_hide,
6665
)
6766
window.dodge_monitor = dodge_monitor
68-
dodge_monitor.start()
67+
if dodge_monitor is not None:
68+
dodge_monitor.start()
6969
return window

docs/WAYLAND.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3160,10 +3160,10 @@ now has the X11 window facade, production X11 runtime wiring, neutral
31603160
`WindowId` values alongside existing XIDs, and menu rows backed by
31613161
`WindowSnapshot`.
31623162

3163-
The next active X11 migration PR is the X11-only session backend shape. It
3164-
should group the already-migrated window and preview services behind an explicit
3165-
session backend without changing current X11 behavior, and it should leave
3166-
visibility and surface ownership to their dedicated later PRs.
3163+
The next active X11 migration PR is the visibility service step. It should move
3164+
X11 dodge monitor ownership behind `VisibilityService` without changing current
3165+
overlap or hide-mode behavior, and it should leave surface ownership to its
3166+
dedicated later PR.
31673167

31683168
An optional temporary fallback can make that step safer:
31693169

@@ -3431,7 +3431,7 @@ Exit criteria:
34313431
- preview behavior remains the same on X11
34323432
- tests cover stale/not-found `WindowId` handling
34333433

3434-
#### [ ] PR 7: Complete Session Backend Shape With X11 Only
3434+
#### [x] PR 7: Complete Session Backend Shape With X11 Only
34353435

34363436
Complete the X11 `SessionBackend` shape after the first runtime service wiring
34373437
and after menu/preview can consume services. Production still selects only the

tests/platform/test_x11_session.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,19 +61,25 @@ def test_build_x11_window_tracker_ignores_invalid_mode(monkeypatch):
6161
def test_build_x11_session_backend_groups_x11_services(monkeypatch):
6262
windows = MagicMock()
6363
previews = MagicMock()
64+
visibility = MagicMock()
65+
config = MagicMock()
6466
monkeypatch.setattr(
6567
session, "build_x11_window_tracker", MagicMock(return_value=windows)
6668
)
6769
monkeypatch.setattr(session, "X11PreviewService", MagicMock(return_value=previews))
70+
monkeypatch.setattr(
71+
session, "X11VisibilityService", MagicMock(return_value=visibility)
72+
)
6873

6974
backend = session.build_x11_session_backend(
70-
model=MagicMock(), launcher=MagicMock(), config=MagicMock()
75+
model=MagicMock(), launcher=MagicMock(), config=config
7176
)
7277

7378
assert backend.name == "x11"
7479
assert backend.display_server is session.DisplayServer.X11
7580
assert backend.windows is windows
7681
assert backend.previews is previews
82+
assert backend.visibility is visibility
7783
assert backend.workspaces is None
7884
assert backend.desktop_actions is None
7985
assert backend.screen_capture is None
@@ -82,16 +88,22 @@ def test_build_x11_session_backend_groups_x11_services(monkeypatch):
8288
assert backend.capabilities.tracks_windows is True
8389
assert backend.capabilities.supports_window_menu is True
8490
assert backend.capabilities.supports_screen_reservation is True
91+
assert backend.capabilities.supports_overlap_active is True
8592
session.X11PreviewService.assert_called_once_with(window_tracker=windows)
93+
session.X11VisibilityService.assert_called_once_with(config=config)
8694

8795

8896
def test_x11_session_backend_lifecycle_starts_and_stops_services(monkeypatch):
8997
windows = MagicMock()
9098
previews = MagicMock()
99+
visibility = MagicMock()
91100
monkeypatch.setattr(
92101
session, "build_x11_window_tracker", MagicMock(return_value=windows)
93102
)
94103
monkeypatch.setattr(session, "X11PreviewService", MagicMock(return_value=previews))
104+
monkeypatch.setattr(
105+
session, "X11VisibilityService", MagicMock(return_value=visibility)
106+
)
95107
backend = session.X11SessionBackend(
96108
model=MagicMock(), launcher=MagicMock(), config=MagicMock()
97109
)
@@ -101,23 +113,31 @@ def test_x11_session_backend_lifecycle_starts_and_stops_services(monkeypatch):
101113

102114
windows.start.assert_called_once_with()
103115
previews.start.assert_called_once_with()
116+
visibility.start.assert_called_once_with()
117+
visibility.stop.assert_called_once_with()
104118
previews.stop.assert_called_once_with()
105119
windows.stop.assert_called_once_with()
106120

107121

108122
def test_x11_session_backend_allows_legacy_tracker_without_lifecycle(monkeypatch):
109123
windows = object()
110124
previews = MagicMock()
125+
visibility = MagicMock()
111126
monkeypatch.setattr(
112127
session, "build_x11_window_tracker", MagicMock(return_value=windows)
113128
)
114129
monkeypatch.setattr(session, "X11PreviewService", MagicMock(return_value=previews))
130+
monkeypatch.setattr(
131+
session, "X11VisibilityService", MagicMock(return_value=visibility)
132+
)
115133
backend = session.X11SessionBackend(
116134
model=MagicMock(), launcher=MagicMock(), config=MagicMock()
117135
)
118136

119137
backend.start()
120138
backend.stop()
121139

140+
visibility.start.assert_called_once_with()
141+
visibility.stop.assert_called_once_with()
122142
previews.start.assert_called_once_with()
123143
previews.stop.assert_called_once_with()

0 commit comments

Comments
 (0)