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:
co-authored by
Jatin K Malik
parent
ac783e53bd
commit
9374b86793
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user