mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-10-28 16:11:27 +01:00
fix(gui): remap inactive Wayland windows when raising
This commit is contained in:
1 parent
341c52e380
commit
d9bcf735d5
3 files changed
+66
-9
No files matched your search
@@ -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:
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user