* fix(overlay): detect Wayland by GType so the fallback overlay stays compact (#1030) _is_wayland() checked isinstance(display, Gdk.WaylandDisplay), but GTK3's Gdk typelib has no WaylandDisplay class, so it always returned False. On Wayland without gtk-layer-shell, _layout_bottom_strip() therefore sized the prewarmed fallback window to the full monitor width (the X11 strip) instead of the compact centered window meant for Wayland, which on KDE showed up as a wide empty "Vocalinux Dictation" window listed in the taskbar. Drop _is_wayland() and use _display_is_wayland(), which checks the GType name and already gates prewarm, so both decisions agree. Also list gtk-layer-shell as an optdepend of the source AUR package: with it KWin and wlroots compositors get a layer surface that is never listed in the taskbar and never needs the prewarmed toplevel. The AppImage (and so vocalinux-bin) already bundles it. * docs(overlay): describe the compact Wayland fallback in the overlay docstrings The class docstring still said the overlay is always a full-width strip, which no longer holds on Wayland without layer shell (#1030). * fix(overlay): keep the full-width strip on GNOME Wayland Mutter ignores CENTER_ALWAYS for a plain Wayland toplevel, so the compact fallback window landed in the top-left corner on GNOME (tested on GNOME Shell 46 with the Ubuntu Dock). The full-width strip at least keeps the orb horizontally centered there. Use the compact window only on KDE, where KWin listed the strip in the taskbar as a wide empty window (#1030).
This commit is contained in:
@@ -56,6 +56,7 @@ optdepends=(
|
|||||||
'xclip: X11 clipboard tools (copy/paste injection fallbacks)'
|
'xclip: X11 clipboard tools (copy/paste injection fallbacks)'
|
||||||
'wl-clipboard: Wayland clipboard (wl-copy/wl-paste) injection fallbacks'
|
'wl-clipboard: Wayland clipboard (wl-copy/wl-paste) injection fallbacks'
|
||||||
'wtype: Wayland keystroke injection fallback'
|
'wtype: Wayland keystroke injection fallback'
|
||||||
|
'gtk-layer-shell: dictation overlay as a layer surface on KDE and wlroots Wayland'
|
||||||
)
|
)
|
||||||
conflicts=('vocalinux-git')
|
conflicts=('vocalinux-git')
|
||||||
source=("${pkgname}-${_version}.tar.gz::https://github.com/VocaHQ/vocalinux/releases/download/v${_tag}/${pkgname}-${_version}.tar.gz")
|
source=("${pkgname}-${_version}.tar.gz::https://github.com/VocaHQ/vocalinux/releases/download/v${_tag}/${pkgname}-${_version}.tar.gz")
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
|
import os
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from typing import TYPE_CHECKING, Callable, Optional
|
from typing import TYPE_CHECKING, Callable, Optional
|
||||||
|
|
||||||
@@ -92,6 +93,16 @@ class DictationOverlayController:
|
|||||||
return MODE_PROCESSING
|
return MODE_PROCESSING
|
||||||
|
|
||||||
|
|
||||||
|
def _is_kde_session() -> bool:
|
||||||
|
"""Whether the session is KDE Plasma (KWin), from the usual desktop variables."""
|
||||||
|
if os.environ.get("KDE_FULL_SESSION", "").lower() == "true":
|
||||||
|
return True
|
||||||
|
desktop = " ".join(
|
||||||
|
os.environ.get(var, "") for var in ("XDG_CURRENT_DESKTOP", "DESKTOP_SESSION", "GDMSESSION")
|
||||||
|
).lower()
|
||||||
|
return "kde" in desktop or "plasma" in desktop
|
||||||
|
|
||||||
|
|
||||||
def _try_import_layer_shell() -> Optional[ModuleType]:
|
def _try_import_layer_shell() -> Optional[ModuleType]:
|
||||||
"""
|
"""
|
||||||
Soft-import GtkLayerShell when available (Wayland, wlroots-based).
|
Soft-import GtkLayerShell when available (Wayland, wlroots-based).
|
||||||
@@ -115,9 +126,11 @@ class DictationOverlay:
|
|||||||
"""
|
"""
|
||||||
Floating glowing dictation indicator backed by a GTK window.
|
Floating glowing dictation indicator backed by a GTK window.
|
||||||
|
|
||||||
Uses a full-width bottom strip so the orb is always bottom-center of the
|
On X11 and with layer shell, a full-width bottom strip keeps the orb
|
||||||
monitor (Wayland often ignores move() on a small centered popup). The
|
bottom-center of the monitor. A plain Wayland toplevel cannot be
|
||||||
window is passive: no keyboard focus, click-through input region.
|
positioned: on KDE it is a compact window KWin places, elsewhere the
|
||||||
|
strip stays so the orb is at least horizontally centered. The window is
|
||||||
|
passive: no keyboard focus, click-through input region.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -312,16 +325,6 @@ class DictationOverlay:
|
|||||||
"""Re-evaluate the hide strategy when a compositor appears or leaves."""
|
"""Re-evaluate the hide strategy when a compositor appears or leaves."""
|
||||||
self._sync_window()
|
self._sync_window()
|
||||||
|
|
||||||
def _is_wayland(self) -> bool:
|
|
||||||
"""True when the default display is a Wayland one."""
|
|
||||||
try:
|
|
||||||
wayland_cls = getattr(self._Gdk, "WaylandDisplay", None)
|
|
||||||
if wayland_cls is None:
|
|
||||||
return False
|
|
||||||
return isinstance(self._Gdk.Display.get_default(), wayland_cls)
|
|
||||||
except Exception:
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _display_is_wayland(self) -> bool:
|
def _display_is_wayland(self) -> bool:
|
||||||
"""Check the GType name; GTK3's Gdk typelib does not expose GdkWaylandDisplay as a class."""
|
"""Check the GType name; GTK3's Gdk typelib does not expose GdkWaylandDisplay as a class."""
|
||||||
display = self._Gdk.Display.get_default()
|
display = self._Gdk.Display.get_default()
|
||||||
@@ -467,13 +470,16 @@ class DictationOverlay:
|
|||||||
|
|
||||||
def _layout_bottom_strip(self) -> None:
|
def _layout_bottom_strip(self) -> None:
|
||||||
"""
|
"""
|
||||||
Size the window as a full-width bottom strip and place it on the primary monitor.
|
Size and place the fallback window (no layer shell) on the primary monitor.
|
||||||
|
|
||||||
Drawing the orb at the strip's horizontal center always yields bottom-center
|
Drawing the orb at the strip's horizontal center always yields bottom-center
|
||||||
of the screen, even when the compositor only roughly honors move().
|
of the screen, even when the compositor only roughly honors move().
|
||||||
On Wayland without layer shell, move() is a hint the compositor can
|
On Wayland without layer shell, move() is a hint the compositor can
|
||||||
ignore entirely, so ask for a centered compact window instead of
|
ignore. KWin lists the full-width strip in the taskbar as a wide empty
|
||||||
promising a bottom position it cannot place.
|
window (#1030), so on KDE ask for a centered compact window instead.
|
||||||
|
Mutter ignores CENTER_ALWAYS and would put a compact window in the
|
||||||
|
top-left corner, so GNOME and other compositors keep the strip, which
|
||||||
|
at least keeps the orb horizontally centered.
|
||||||
"""
|
"""
|
||||||
if self._window is None:
|
if self._window is None:
|
||||||
return
|
return
|
||||||
@@ -488,8 +494,8 @@ class DictationOverlay:
|
|||||||
return
|
return
|
||||||
geom = monitor.get_geometry()
|
geom = monitor.get_geometry()
|
||||||
height = _STRIP_HEIGHT
|
height = _STRIP_HEIGHT
|
||||||
wayland = self._is_wayland()
|
compact = self._display_is_wayland() and _is_kde_session()
|
||||||
if wayland:
|
if compact:
|
||||||
self._window.set_position(self._Gtk.WindowPosition.CENTER_ALWAYS)
|
self._window.set_position(self._Gtk.WindowPosition.CENTER_ALWAYS)
|
||||||
width = _ORB_SIZE
|
width = _ORB_SIZE
|
||||||
else:
|
else:
|
||||||
@@ -497,7 +503,7 @@ class DictationOverlay:
|
|||||||
self._window.resize(width, height)
|
self._window.resize(width, height)
|
||||||
if self._drawing_area is not None:
|
if self._drawing_area is not None:
|
||||||
self._drawing_area.set_size_request(width, height)
|
self._drawing_area.set_size_request(width, height)
|
||||||
if not wayland:
|
if not compact:
|
||||||
x = geom.x
|
x = geom.x
|
||||||
y = geom.y + geom.height - height - _BOTTOM_MARGIN
|
y = geom.y + geom.height - height - _BOTTOM_MARGIN
|
||||||
self._window.move(x, y)
|
self._window.move(x, y)
|
||||||
|
|||||||
@@ -592,6 +592,7 @@ class TestOverlayWithMockedGtkWindow(unittest.TestCase):
|
|||||||
monitor = MagicMock()
|
monitor = MagicMock()
|
||||||
monitor.get_geometry.return_value = geom
|
monitor.get_geometry.return_value = geom
|
||||||
display = MagicMock()
|
display = MagicMock()
|
||||||
|
display.__gtype__ = SimpleNamespace(name="GdkX11Display")
|
||||||
display.get_primary_monitor.return_value = monitor
|
display.get_primary_monitor.return_value = monitor
|
||||||
overlay._Gdk = MagicMock()
|
overlay._Gdk = MagicMock()
|
||||||
overlay._Gdk.Display.get_default.return_value = display
|
overlay._Gdk.Display.get_default.return_value = display
|
||||||
@@ -601,6 +602,55 @@ class TestOverlayWithMockedGtkWindow(unittest.TestCase):
|
|||||||
mock_window.resize.assert_called_once_with(1920, _STRIP_HEIGHT)
|
mock_window.resize.assert_called_once_with(1920, _STRIP_HEIGHT)
|
||||||
mock_window.move.assert_called_once_with(100, 50 + 1080 - _STRIP_HEIGHT - _BOTTOM_MARGIN)
|
mock_window.move.assert_called_once_with(100, 50 + 1080 - _STRIP_HEIGHT - _BOTTOM_MARGIN)
|
||||||
|
|
||||||
|
def _layout_on_wayland(self, desktop: str) -> DictationOverlay:
|
||||||
|
"""Lay out the no-layer-shell fallback on a 2560x1600 Wayland monitor.
|
||||||
|
|
||||||
|
GTK3's Gdk typelib has no ``WaylandDisplay`` class, so the display
|
||||||
|
must be recognized by its GType name, as ``_display_is_wayland`` does.
|
||||||
|
"""
|
||||||
|
overlay = _overlay_without_gtk(enabled=True)
|
||||||
|
overlay._window = MagicMock()
|
||||||
|
overlay._drawing_area = MagicMock()
|
||||||
|
geom = MagicMock(x=0, y=0, width=2560, height=1600)
|
||||||
|
monitor = MagicMock()
|
||||||
|
monitor.get_geometry.return_value = geom
|
||||||
|
display = MagicMock()
|
||||||
|
display.__gtype__ = SimpleNamespace(name="GdkWaylandDisplay")
|
||||||
|
display.get_primary_monitor.return_value = monitor
|
||||||
|
# Mirror the real GTK3 typelib: Gdk has Display but no WaylandDisplay.
|
||||||
|
overlay._Gdk = SimpleNamespace(Display=MagicMock())
|
||||||
|
overlay._Gdk.Display.get_default.return_value = display
|
||||||
|
overlay._Gtk = MagicMock()
|
||||||
|
env = {"XDG_CURRENT_DESKTOP": desktop, "DESKTOP_SESSION": "", "GDMSESSION": ""}
|
||||||
|
with patch.dict(os.environ, env):
|
||||||
|
os.environ.pop("KDE_FULL_SESSION", None)
|
||||||
|
overlay._layout_bottom_strip()
|
||||||
|
return overlay
|
||||||
|
|
||||||
|
def test_layout_wayland_fallback_on_kde_is_compact_and_centered(self):
|
||||||
|
"""KWin lists the full-width strip in the taskbar; KDE gets a compact window (#1030)."""
|
||||||
|
from vocalinux.ui.dictation_overlay import _ORB_SIZE, _STRIP_HEIGHT
|
||||||
|
|
||||||
|
overlay = self._layout_on_wayland("KDE")
|
||||||
|
|
||||||
|
overlay._window.resize.assert_called_once_with(_ORB_SIZE, _STRIP_HEIGHT)
|
||||||
|
overlay._window.set_position.assert_called_once_with(
|
||||||
|
overlay._Gtk.WindowPosition.CENTER_ALWAYS
|
||||||
|
)
|
||||||
|
overlay._window.move.assert_not_called()
|
||||||
|
|
||||||
|
def test_layout_wayland_fallback_on_gnome_keeps_full_width_strip(self):
|
||||||
|
"""Mutter ignores CENTER_ALWAYS, so a compact window would sit top-left.
|
||||||
|
|
||||||
|
GNOME keeps the full-width strip, which leaves the orb horizontally centered.
|
||||||
|
"""
|
||||||
|
from vocalinux.ui.dictation_overlay import _STRIP_HEIGHT
|
||||||
|
|
||||||
|
overlay = self._layout_on_wayland("ubuntu:GNOME")
|
||||||
|
|
||||||
|
overlay._window.resize.assert_called_once_with(2560, _STRIP_HEIGHT)
|
||||||
|
overlay._window.set_position.assert_not_called()
|
||||||
|
|
||||||
def test_layout_falls_back_to_first_monitor(self):
|
def test_layout_falls_back_to_first_monitor(self):
|
||||||
overlay = _overlay_without_gtk(enabled=True)
|
overlay = _overlay_without_gtk(enabled=True)
|
||||||
mock_window = MagicMock()
|
mock_window = MagicMock()
|
||||||
|
|||||||
Reference in New Issue
Block a user