Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 65 additions & 11 deletions graphlink_app/graphlink_context_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

from __future__ import annotations

import atexit

from PySide6.QtCore import QEvent, QObject, QTimer, Qt
from PySide6.QtGui import QColor, QPalette
from PySide6.QtWidgets import QApplication, QMenu, QWidget
Expand Down Expand Up @@ -100,26 +102,62 @@ def __init__(self, menu: QMenu):
self._timer.timeout.connect(self._enforce)

def eventFilter(self, watched, event):
if watched is self.menu and event.type() in {
QEvent.Type.Polish,
QEvent.Type.Show,
QEvent.Type.StyleChange,
}:
# QStyleSheetStyle can run after the event filter. Defer one
# turn so the final visible popup state is the opaque state.
self._timer.start(0)
try:
if watched is self.menu and event.type() in {
QEvent.Type.Polish,
QEvent.Type.Show,
QEvent.Type.StyleChange,
}:
# QStyleSheetStyle can run after the event filter. Defer one
# turn so the final visible popup state is the opaque state.
self._timer.start(0)
except (AttributeError, RuntimeError, SystemError, TypeError):
# PySide can dispatch a final event while its C++ wrappers are
# already being torn down. The filter must never turn shutdown
# into a failing process exit.
return False
return False

def _enforce(self):
_enforce_opaque_surface(self.menu)
try:
_enforce_opaque_surface(self.menu)
except (AttributeError, RuntimeError, SystemError, TypeError):
return


class _ApplicationContextMenuFilter(QObject):
"""Configure QMenus created internally by Qt (for example text editors)."""

def eventFilter(self, watched, event):
if isinstance(watched, QMenu) and event.type() == QEvent.Type.Show:
configure_context_menu(watched)
try:
if _is_qmenu(watched) and event.type() == QEvent.Type.Show:
configure_context_menu(watched)
except (AttributeError, RuntimeError, SystemError, TypeError):
# During interpreter shutdown Shiboken may tear down QMenu's type
# object before QApplication stops dispatching its event filter.
# Treat that late callback as a no-op instead of leaking a Python
# exception through QObject::eventFilter and failing pytest.
return False
return False

def detach(self):
"""Remove this filter while QApplication and its wrappers are alive."""
try:
app = self.parent()
if app is None:
return
app.removeEventFilter(self)
if getattr(app, "_graphlink_context_menu_filter", None) is self:
app._graphlink_context_menu_filter = None
except (AttributeError, RuntimeError, SystemError, TypeError):
return


def _is_qmenu(watched, menu_type=QMenu):
"""Return whether ``watched`` is a menu, including during Qt teardown."""
try:
return isinstance(watched, menu_type)
except (RuntimeError, SystemError, TypeError):
return False


Expand Down Expand Up @@ -163,6 +201,22 @@ def install_context_menu_filter(app: QApplication) -> QObject:
guard = _ApplicationContextMenuFilter(app)
app._graphlink_context_menu_filter = guard
app.installEventFilter(guard)
app.aboutToQuit.connect(guard.detach)

# pytest and some embedded hosts do not run the normal Qt quit path.
# Register cleanup before PySide's module-shutdown handler so the
# application filter is gone before Shiboken destroys QMenu's type.
if not getattr(app, "_graphlink_context_menu_atexit", False):
def _cleanup():
try:
current = getattr(app, "_graphlink_context_menu_filter", None)
if current is not None:
current.detach()
except (AttributeError, RuntimeError, SystemError, TypeError):
return

atexit.register(_cleanup)
app._graphlink_context_menu_atexit = True
return guard


Expand Down
18 changes: 18 additions & 0 deletions graphlink_app/tests/test_context_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from PySide6.QtWidgets import QApplication, QMenu, QPlainTextEdit

from graphlink_context_menu import (
_is_qmenu,
configure_context_menu,
create_context_menu,
install_context_menu_filter,
Expand Down Expand Up @@ -54,3 +55,20 @@ def test_application_filter_configures_qt_created_editor_context_menus():

menu.close()
editor.close()


def test_application_filter_ignores_shiboken_type_teardown():
class _ExplodingQMenuMeta(type):
def __instancecheck__(cls, instance):
raise SystemError("QMenu wrapper is already shutting down")

class _ExplodingQMenu(metaclass=_ExplodingQMenuMeta):
pass

assert _is_qmenu(object(), _ExplodingQMenu) is False


def test_application_filter_can_be_detached_before_qt_shutdown():
guard = install_context_menu_filter(_APP)
guard.detach()
assert getattr(_APP, "_graphlink_context_menu_filter", None) is None
Loading