fix(overlay): keep the Wayland fallback overlay alive when the dock closes it (#1017)
Refuse close requests on the pre-mapped Wayland overlay so closing its GNOME dock entry no longer destroys it and brings back the first-recording focus loss, and unmap the overlay when it is turned off so the dock entry goes away immediately. Fixes #1010
This commit is contained in:
@@ -14,7 +14,7 @@ from types import ModuleType
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk, Gtk
|
||||
|
||||
from ..common_types import RecognitionState
|
||||
|
||||
@@ -209,6 +209,7 @@ class DictationOverlay:
|
||||
window.connect("size-allocate", self._on_size_allocate)
|
||||
window.connect("realize", self._on_realize)
|
||||
window.connect("map-event", self._on_map_event)
|
||||
window.connect("delete-event", self._on_delete_event)
|
||||
# Fallback placement is stale the moment monitor geometry or the
|
||||
# compositor changes; layer shell re-anchors itself on these signals.
|
||||
screen.connect("size-changed", self._on_screen_geometry_changed)
|
||||
@@ -254,6 +255,21 @@ class DictationOverlay:
|
||||
self._on_realize(widget)
|
||||
return False
|
||||
|
||||
def _on_delete_event(self, widget: Gtk.Widget, event: Gdk.Event) -> bool:
|
||||
"""Refuse close requests from the dock or taskbar.
|
||||
|
||||
On Wayland without layer shell the overlay is a regular toplevel, so
|
||||
GNOME's dock lists it and can close it. GTK's default handler would
|
||||
destroy the window; the next recording would then map a new toplevel
|
||||
that takes focus from the editor (#1010). Disabling the overlay in
|
||||
Settings unmaps it instead.
|
||||
"""
|
||||
logger.info(
|
||||
"Ignoring a close request for the dictation overlay; "
|
||||
"turn the overlay off in Settings to remove it"
|
||||
)
|
||||
return True
|
||||
|
||||
def _on_size_allocate(self, widget, allocation) -> None:
|
||||
"""Keep the empty input shape after size changes."""
|
||||
if widget.get_realized():
|
||||
@@ -336,6 +352,13 @@ class DictationOverlay:
|
||||
mode = self.controller.mode
|
||||
if mode is None:
|
||||
self._stop_animation()
|
||||
if not self.controller.enabled:
|
||||
# Turned off: nothing maps the window again until it is
|
||||
# re-enabled, so unmap it. A transparent mapped window still
|
||||
# shows in the dock on Wayland without layer shell (#1010).
|
||||
if self._window.get_visible():
|
||||
self._window.hide()
|
||||
return
|
||||
# Opacity 0 instead of hide() — hide/show cycles re-activate the
|
||||
# window on many Wayland compositors and steal editor focus (which
|
||||
# breaks IBus/wtype injection into the previously focused app).
|
||||
|
||||
@@ -371,6 +371,53 @@ class TestOverlayWithMockedGtkWindow(unittest.TestCase):
|
||||
mock_prewarm.assert_called_once_with()
|
||||
overlay.destroy()
|
||||
|
||||
def test_close_request_keeps_overlay_window(self) -> None:
|
||||
"""A close from GNOME's dock must not destroy the prewarmed window (#1010)."""
|
||||
overlay, mock_window = self._mock_window_overlay()
|
||||
|
||||
self.assertTrue(overlay._on_delete_event(mock_window, MagicMock()))
|
||||
|
||||
mock_window.destroy.assert_not_called()
|
||||
mock_window.hide.assert_not_called()
|
||||
overlay._window = None
|
||||
overlay.destroy()
|
||||
|
||||
def test_disabling_unmaps_prewarmed_window(self) -> None:
|
||||
"""Turning the overlay off removes the transparent window from the dock (#1010)."""
|
||||
overlay, mock_window = self._mock_window_overlay()
|
||||
overlay._use_layer_shell = False
|
||||
mock_window.is_composited.return_value = True
|
||||
|
||||
with (
|
||||
patch.object(overlay, "_display_is_wayland", return_value=True),
|
||||
patch.object(overlay, "_layout_bottom_strip"),
|
||||
):
|
||||
overlay.prewarm()
|
||||
mock_window.get_visible.return_value = True
|
||||
|
||||
overlay.set_enabled(False)
|
||||
mock_window.hide.assert_called_once()
|
||||
|
||||
mock_window.get_visible.return_value = False
|
||||
overlay.set_enabled(True)
|
||||
|
||||
self.assertEqual(mock_window.show_all.call_count, 2)
|
||||
mock_window.set_opacity.assert_called_with(0.0)
|
||||
overlay._window = None
|
||||
overlay.destroy()
|
||||
|
||||
def test_idle_keeps_enabled_window_mapped_when_composited(self) -> None:
|
||||
overlay, mock_window = self._mock_window_overlay()
|
||||
mock_window.get_visible.return_value = True
|
||||
mock_window.is_composited.return_value = True
|
||||
|
||||
overlay.on_recognition_state(RecognitionState.IDLE)
|
||||
|
||||
mock_window.hide.assert_not_called()
|
||||
mock_window.set_opacity.assert_called_with(0.0)
|
||||
overlay._window = None
|
||||
overlay.destroy()
|
||||
|
||||
def test_sync_shows_on_listening_hides_on_idle(self):
|
||||
overlay = _overlay_without_gtk(enabled=True)
|
||||
mock_window = MagicMock()
|
||||
@@ -881,6 +928,7 @@ class TestInitGtkWindowWithMocks(unittest.TestCase):
|
||||
win.add.assert_called_with(mocks["drawing"])
|
||||
mocks["drawing"].connect.assert_any_call("draw", overlay._on_draw)
|
||||
win.connect.assert_any_call("realize", overlay._on_realize)
|
||||
win.connect.assert_any_call("delete-event", overlay._on_delete_event)
|
||||
self.assertFalse(overlay._use_layer_shell)
|
||||
overlay.destroy()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user