From 4497bfae6c514f555309cb4d94ebbd89efc083b5 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 30 Sep 2026 05:55:33 -0700 Subject: [PATCH] fix(logs): let the View Logs dialog close via titlebar-X (#892) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(logs): let the View Logs dialog close via titlebar-X _on_response only destroyed on ResponseType.CLOSE and ignored DELETE_EVENT, so the Logs window could not be dismissed with the window-manager close button — unlike Settings, which handles both. A window force-destroyed behind the dialog then crashed quit with a BadWindow error instead of an orderly shutdown. * test(logs): cover _on_response behavior; annotate its signature --------- Co-authored-by: Jatin K Malik --- src/vocalinux/ui/logging_dialog.py | 4 +-- tests/test_logging_dialog.py | 44 ++++++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/vocalinux/ui/logging_dialog.py b/src/vocalinux/ui/logging_dialog.py index 8eb73a0..2f18345 100644 --- a/src/vocalinux/ui/logging_dialog.py +++ b/src/vocalinux/ui/logging_dialog.py @@ -622,9 +622,9 @@ class LoggingDialog(Gtk.Dialog): return False # Remove from idle queue - def _on_response(self, dialog, response_id): + def _on_response(self, dialog: Gtk.Dialog, response_id: int) -> None: """Handle dialog responses.""" - if response_id == Gtk.ResponseType.CLOSE: + if response_id in (Gtk.ResponseType.CLOSE, Gtk.ResponseType.DELETE_EVENT): self.destroy() def _export_logs(self): diff --git a/tests/test_logging_dialog.py b/tests/test_logging_dialog.py index b338b61..90ab7bf 100644 --- a/tests/test_logging_dialog.py +++ b/tests/test_logging_dialog.py @@ -94,6 +94,50 @@ class TestLoggingDialogClass(unittest.TestCase): """Test that dialog unregisters callback on destroy.""" self.assertIn("unregister_callback(self._on_new_log_record)", self.source_code) + def test_response_handler_closes_on_delete_event(self) -> None: + """Titlebar-X (DELETE_EVENT) destroys the dialog like Close does.""" + import ast + from types import SimpleNamespace + from unittest.mock import MagicMock + + # conftest mocks gi globally, so `class LoggingDialog(Gtk.Dialog)` + # resolves to a Mock at import time and the real handler is + # unreachable. Compile the actual _on_response source node and exec + # it against a real Gtk namespace — this runs the true code path. + class _FakeGtk: + Dialog = object + ResponseType = SimpleNamespace(CLOSE=-7, DELETE_EVENT=-4, CANCEL=-6) + + def __getattr__(self, _name: str) -> type: + return object + + tree = ast.parse(self.source_code) + func_node = next( + node + for cls in tree.body + if isinstance(cls, ast.ClassDef) and cls.name == "LoggingDialog" + for node in cls.body + if isinstance(node, ast.FunctionDef) and node.name == "_on_response" + ) + module = ast.Module(body=[func_node], type_ignores=[]) + namespace: dict = {"Gtk": _FakeGtk()} + exec(compile(ast.fix_missing_locations(module), "", "exec"), namespace) + handler = namespace["_on_response"] + + dialog = SimpleNamespace() + for response_id, expected in ( + (_FakeGtk.ResponseType.CLOSE, True), + (_FakeGtk.ResponseType.DELETE_EVENT, True), + (_FakeGtk.ResponseType.CANCEL, False), + ): + dialog.destroy = MagicMock() + handler(dialog, dialog, response_id) + self.assertIs( + dialog.destroy.called, + expected, + f"response {response_id}: destroy called={dialog.destroy.called}", + ) + class TestLoggingDialogFilterBar(unittest.TestCase): """Test cases for LoggingDialog filter bar."""