fix(logs): let the View Logs dialog close via titlebar-X (#892)

* 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 <jatinkrmalik@gmail.com>
This commit is contained in:
devin-ai-integration[bot]
2026-09-30 05:55:33 -07:00
committed by GitHub
co-authored by Jatin K Malik
parent d11bd1bf0d
commit 4497bfae6c
2 changed files with 46 additions and 2 deletions
+2 -2
View File
@@ -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):
+44
View File
@@ -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), "<test>", "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."""