fix(gui): remap inactive Wayland windows when raising

This commit is contained in:
wyzula_j committed 2026-09-10 14:29:32 +02:00
1 parent 341c52e380
commit d9bcf735d5
3 files changed
+66 -9

No files matched your search

+6 -3
View File
@@ -106,9 +106,12 @@ when a window is behind another application. `gui.raise_window()` uses the same
`gui.flomni.raise_window()` targets one window. Already-visible, active windows are left unchanged.
On XCB (X11/XWayland), BEC retains its GNOME/RHEL window-raising workaround and restores the original
window flags afterwards. On native Wayland and macOS it requests activation without changing flags.
Activation is subject to the window manager/compositor's focus policy, particularly on Wayland;
a Redis RPC is not a local mouse or keyboard event.
window flags afterwards. On native Wayland, BEC briefly hides and shows visible, inactive windows
before requesting activation, because GNOME can refuse to activate an existing window. This keeps
the window's contents and flags, but may cause flicker or let the compositor reposition it. Each
call finishes with the window shown; already-active windows are left untouched. On macOS, BEC
requests activation without remapping the window. Activation is still subject to the window
manager/compositor's focus policy; a Redis RPC is not a local mouse or keyboard event.
To inspect the **running GUI server process**, from the BEC IPython client:
+6 -3
View File
@@ -265,7 +265,8 @@ class RPCServer:
return
state = window.windowState() & ~Qt.WindowMinimized
if QApplication.platformName() == "xcb":
platform = QApplication.platformName()
if platform == "xcb":
# Keep the GNOME/RHEL X11 workaround that also works through XWayland.
# Never run it on native Wayland: flag changes hide/recreate the surface.
flags = window.windowFlags()
@@ -282,10 +283,12 @@ class RPCServer:
if window.isMinimized():
window.setWindowState(state)
if platform.startswith("wayland") and window.isVisible():
# GNOME Wayland can refuse activation of an existing surface. Remap the
# inactive window using hide/show, which brings it forward on those consoles.
window.hide()
if not window.isVisible():
window.show()
# Qt requests activation from the compositor on Wayland. A visible window can
# still be behind another application, so visibility alone must not skip this.
window.raise_()
window.activateWindow()
+54 -3
View File
@@ -101,18 +101,21 @@ class WindowEvents(QObject):
def __init__(self, widget):
super().__init__(widget)
self.hide_events = 0
self.show_events = 0
widget.installEventFilter(self)
def eventFilter(self, watched, event):
if event.type() == QEvent.Hide:
self.hide_events += 1
elif event.type() == QEvent.Show:
self.show_events += 1
return False
@pytest.mark.parametrize("method", ["show", "raise"])
@pytest.mark.parametrize("stay_on_top", [False, True])
@pytest.mark.parametrize("platform", ["wayland", "wayland-egl", "cocoa"])
def test_repeated_show_and_raise_do_not_hide_visible_window(
def test_repeated_show_and_raise_preserve_visibility_and_flags(
rpc_server, dummy_widget, qapp, monkeypatch, method, stay_on_top, platform
):
monkeypatch.setattr(rpc_server_module.QApplication, "platformName", lambda: platform)
@@ -129,7 +132,8 @@ def test_repeated_show_and_raise_do_not_hide_visible_window(
qapp.processEvents()
assert dummy_widget.isVisible()
assert dummy_widget.windowFlags() == flags
assert events.hide_events == 0
expected_remaps = 3 if method == "raise" and platform.startswith("wayland") else 0
assert events.hide_events == events.show_events == expected_remaps
def test_widget_show_visible_window_does_not_request_activation(rpc_server, dummy_widget):
@@ -195,9 +199,10 @@ def test_raise_active_window_is_noop(rpc_server, dummy_widget, monkeypatch, plat
patch.object(dummy_widget, "raise_") as raise_window,
patch.object(dummy_widget, "activateWindow") as activate,
patch.object(dummy_widget, "show") as show,
patch.object(dummy_widget, "hide") as hide,
):
rpc_server.run_rpc(dummy_widget, "raise", [], {})
for operation in (flags, state, raise_window, activate, show):
for operation in (flags, state, raise_window, activate, show, hide):
operation.assert_not_called()
@@ -217,6 +222,52 @@ def test_raise_visible_inactive_window_requests_activation(
flags.assert_not_called()
@pytest.mark.parametrize("platform", ["wayland", "wayland-egl"])
@pytest.mark.parametrize("initially_visible", [False, True])
def test_wayland_raise_finishes_showing_before_requesting_activation(
rpc_server, dummy_widget, monkeypatch, platform, initially_visible
):
monkeypatch.setattr(rpc_server_module.QApplication, "platformName", lambda: platform)
monkeypatch.setattr(dummy_widget, "isActiveWindow", lambda: False)
dummy_widget.setVisible(initially_visible)
events = WindowEvents(dummy_widget)
visible_during_activation = []
with patch.object(
dummy_widget,
"activateWindow",
side_effect=lambda: visible_during_activation.append(dummy_widget.isVisible()),
):
rpc_server.run_rpc(dummy_widget, "raise", [], {})
assert events.hide_events == int(initially_visible)
assert events.show_events == 1
assert visible_during_activation == [True]
assert dummy_widget.isVisible()
def test_wayland_remap_preserves_dock_contents(rpc_server, qtbot, qapp, monkeypatch, tmp_path):
monkeypatch.setenv("BECWIDGETS_PROFILE_DIR", str(tmp_path))
dock_area = rpc_server._launch_dock_area(name="flomni", startup_profile="skip")
window = dock_area.window()
qtbot.addWidget(window)
content = dock_area.new("TextBox", object_name="status")
qapp.processEvents()
widgets = dock_area.widget_map()
size = window.size()
monkeypatch.setattr(rpc_server_module.QApplication, "platformName", lambda: "wayland")
monkeypatch.setattr(window, "isActiveWindow", lambda: False)
for _ in range(3):
rpc_server.run_rpc(window, "raise", [], {})
qapp.processEvents()
assert window.isVisible()
assert window.size() == size
assert content.isVisible()
assert dock_area.widget_map() == widgets
assert rpc_server.rpc_register.get_rpc_by_id(dock_area.gui_id) is dock_area
@pytest.mark.parametrize("stay_on_top", [False, True])
def test_xcb_raise_preserves_workaround_and_original_flags(
rpc_server, dummy_widget, monkeypatch, stay_on_top