* 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)'
|
||||
'wl-clipboard: Wayland clipboard (wl-copy/wl-paste) injection fallbacks'
|
||||
'wtype: Wayland keystroke injection fallback'
|
||||
'gtk-layer-shell: dictation overlay as a layer surface on KDE and wlroots Wayland'
|
||||
)
|
||||
conflicts=('vocalinux-git')
|
||||
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 math
|
||||
import os
|
||||
from types import ModuleType
|
||||
from typing import TYPE_CHECKING, Callable, Optional
|
||||
|
||||
@@ -92,6 +93,16 @@ class DictationOverlayController:
|
||||
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]:
|
||||
"""
|
||||
Soft-import GtkLayerShell when available (Wayland, wlroots-based).
|
||||
@@ -115,9 +126,11 @@ class DictationOverlay:
|
||||
"""
|
||||
Floating glowing dictation indicator backed by a GTK window.
|
||||
|
||||
Uses a full-width bottom strip so the orb is always bottom-center of the
|
||||
monitor (Wayland often ignores move() on a small centered popup). The
|
||||
window is passive: no keyboard focus, click-through input region.
|
||||
On X11 and with layer shell, a full-width bottom strip keeps the orb
|
||||
bottom-center of the monitor. A plain Wayland toplevel cannot be
|
||||
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__(
|
||||
@@ -312,16 +325,6 @@ class DictationOverlay:
|
||||
"""Re-evaluate the hide strategy when a compositor appears or leaves."""
|
||||
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:
|
||||
"""Check the GType name; GTK3's Gdk typelib does not expose GdkWaylandDisplay as a class."""
|
||||
display = self._Gdk.Display.get_default()
|
||||
@@ -467,13 +470,16 @@ class DictationOverlay:
|
||||
|
||||
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
|
||||
of the screen, even when the compositor only roughly honors move().
|
||||
On Wayland without layer shell, move() is a hint the compositor can
|
||||
ignore entirely, so ask for a centered compact window instead of
|
||||
promising a bottom position it cannot place.
|
||||
ignore. KWin lists the full-width strip in the taskbar as a wide empty
|
||||
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:
|
||||
return
|
||||
@@ -488,8 +494,8 @@ class DictationOverlay:
|
||||
return
|
||||
geom = monitor.get_geometry()
|
||||
height = _STRIP_HEIGHT
|
||||
wayland = self._is_wayland()
|
||||
if wayland:
|
||||
compact = self._display_is_wayland() and _is_kde_session()
|
||||
if compact:
|
||||
self._window.set_position(self._Gtk.WindowPosition.CENTER_ALWAYS)
|
||||
width = _ORB_SIZE
|
||||
else:
|
||||
@@ -497,7 +503,7 @@ class DictationOverlay:
|
||||
self._window.resize(width, height)
|
||||
if self._drawing_area is not None:
|
||||
self._drawing_area.set_size_request(width, height)
|
||||
if not wayland:
|
||||
if not compact:
|
||||
x = geom.x
|
||||
y = geom.y + geom.height - height - _BOTTOM_MARGIN
|
||||
self._window.move(x, y)
|
||||
|
||||
@@ -592,6 +592,7 @@ class TestOverlayWithMockedGtkWindow(unittest.TestCase):
|
||||
monitor = MagicMock()
|
||||
monitor.get_geometry.return_value = geom
|
||||
display = MagicMock()
|
||||
display.__gtype__ = SimpleNamespace(name="GdkX11Display")
|
||||
display.get_primary_monitor.return_value = monitor
|
||||
overlay._Gdk = MagicMock()
|
||||
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.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):
|
||||
overlay = _overlay_without_gtk(enabled=True)
|
||||
mock_window = MagicMock()
|
||||
|
||||
Reference in New Issue
Block a user