fix(ui): keep standalone dialogs from binding to other windows on Wayland (#1024)

On GTK's Wayland backend, a toplevel with the DIALOG type hint and no
transient parent becomes an orphan dialog: the next toplevel to receive
keyboard focus silently becomes its xdg_toplevel parent, and GNOME then
raises, minimizes, and workspace-moves the two windows as one.

Settings, the transcript viewer, and the log viewer are standalone
windows opened parentless, so the dictation pad (focused constantly)
adopted them, making Alt+Tab group them and minimize/workspace changes
apply to both. Force the NORMAL type hint on all three so they stay
independent.

Fixes #984

Co-authored-by: Jatin K Malik <jatinkrmalik@gmail.com>
This commit is contained in:
devin-ai-integration[bot]
2026-10-26 20:28:51 -07:00
committed by GitHub
co-authored by Jatin K Malik
parent ac783e53bd
commit 9374b86793
4 changed files with 68 additions and 0 deletions
+5
View File
@@ -192,6 +192,11 @@ class LoggingDialog(Gtk.Dialog):
flags=Gtk.DialogFlags.DESTROY_WITH_PARENT,
modal=False,
)
# Standalone viewer window: without this the DIALOG type hint makes a
# parentless dialog an "orphan" that GTK's Wayland backend binds to the
# next focused toplevel (usually the dictation pad), so the two windows
# raise, minimize and travel workspaces together (#984).
self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
self.logging_manager = get_logging_manager()
self.auto_scroll = True
+7
View File
@@ -2525,6 +2525,13 @@ class SettingsDialog(AboutSectionMixin):
# has a visible way to dismiss it, even on window managers that hide
# the title-bar close button for Gtk.Dialog windows (fixes #323).
self.set_decorated(True)
# A parentless Gtk.Dialog carries the DIALOG type hint, which lands it
# on GTK's Wayland "orphan dialog" list: the next toplevel to receive
# keyboard focus silently becomes its xdg_toplevel parent, and GNOME
# then minimizes, raises and moves the two windows as a unit (#984).
# Settings is a standalone window, so keep the NORMAL hint and stay
# independent of the dictation pad and overlay.
self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
self.config_manager = config_manager
self.speech_engine = speech_engine
+5
View File
@@ -82,6 +82,11 @@ class TranscriptDialog(Gtk.Dialog):
flags=Gtk.DialogFlags.DESTROY_WITH_PARENT,
modal=False,
)
# Standalone viewer window: without this the DIALOG type hint makes a
# parentless dialog an "orphan" that GTK's Wayland backend binds to the
# next focused toplevel (usually the dictation pad), so the two windows
# raise, minimize and travel workspaces together (#984).
self.set_type_hint(Gdk.WindowTypeHint.NORMAL)
_setup_css()
self.set_default_size(720, 560)
+51
View File
@@ -0,0 +1,51 @@
"""
Regression tests for #984: standalone windows must not carry the DIALOG
type hint.
On GTK's Wayland backend, a toplevel with the DIALOG hint and no transient
parent becomes an "orphan dialog": the next toplevel in the app to receive
keyboard focus silently becomes its xdg_toplevel parent. GNOME then treats
the pair as one window for minimize/restore, raising, and workspace moves.
The dictation pad is a plain Gtk.Window (NORMAL hint) that is focused
constantly, so any standalone Gtk.Dialog left parentless gets bound to it.
Settings, the transcript viewer, and the log viewer are standalone windows
and must opt out with the NORMAL hint.
"""
import os
import unittest
def _read_source(filename: str) -> str:
"""Read a UI module's source code."""
source_path = os.path.join(os.path.dirname(__file__), "..", "src", "vocalinux", "ui", filename)
with open(source_path, "r") as f:
return f.read()
class TestStandaloneDialogsUseNormalHint(unittest.TestCase):
"""Parentless standalone dialogs must not stay DIALOG-hinted (#984)."""
def _assert_normal_hint(self, filename: str, class_name: str) -> None:
source = _read_source(filename)
class_start = source.index(f"class {class_name}(")
init_start = source.index("def __init__", class_start)
# The hint must be reset inside __init__ before the window is shown.
init_end = source.index("\n def ", init_start + 1)
init_source = source[init_start:init_end]
self.assertIn(
"set_type_hint(Gdk.WindowTypeHint.NORMAL)",
init_source,
f"{class_name} must opt out of the DIALOG type hint so GTK's "
"Wayland backend never adopts it as an orphan dialog (#984)",
)
def test_settings_dialog_uses_normal_hint(self) -> None:
self._assert_normal_hint("settings_dialog.py", "SettingsDialog")
def test_transcript_dialog_uses_normal_hint(self) -> None:
self._assert_normal_hint("transcript_dialog.py", "TranscriptDialog")
def test_logging_dialog_uses_normal_hint(self) -> None:
self._assert_normal_hint("logging_dialog.py", "LoggingDialog")