diff --git a/source/JABHandler.py b/source/JABHandler.py index a2f74d652d6..a0751699e2a 100644 --- a/source/JABHandler.py +++ b/source/JABHandler.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2007-2025 NV Access Limited, Peter Vágner, Renaud Paquay, Babbage B.V. +# Copyright (C) 2007-2026 NV Access Limited, Peter Vágner, Renaud Paquay, Babbage B.V. # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -548,7 +548,34 @@ def _fixBridgeFuncs(): def internalQueueFunction(func, *args, **kwargs): - internalFunctionQueue.put_nowait((func, args, kwargs)) + """Queue a function for execution on the main thread. + + When the queue is full, the oldest event is evicted to make room. + JOBJECT64 handles owned by evicted events are released to avoid leaks. + + .. note:: + All queued handler functions except :func:`enterJavaWindow_helper` + follow the convention ``(vmID, accContext, ...)``, where ``args[1]`` + is a JOBJECT64 handle. If a new handler with a different signature + is added, the eviction cleanup below must be updated. + """ + try: + internalFunctionQueue.put_nowait((func, args, kwargs)) + except queue.Full: + try: + evictedFunc, evictedArgs, _evictedKwargs = internalFunctionQueue.get_nowait() + # Release the JOBJECT64 accContext handle from the evicted event. + # All handlers except enterJavaWindow_helper have args[1] as accContext. + if evictedFunc is not enterJavaWindow_helper and len(evictedArgs) >= 2: + bridgeDll.releaseJavaObject(evictedArgs[0], evictedArgs[1]) + except queue.Empty: + pass + try: + internalFunctionQueue.put_nowait((func, args, kwargs)) + except queue.Full: + log.debugWarning("JAB internal function queue full, failed to re-queue after eviction") + return + log.debugWarning("JAB internal function queue full, evicted oldest event") core.requestPump() @@ -959,6 +986,8 @@ def internal_event_activeDescendantChange(vmID, event, source, oldDescendant, ne sourceContext = JABContext(hwnd=hwnd, vmID=vmID, accContext=source) if internal_hasFocus(sourceContext): internalQueueFunction(event_gainFocus, vmID, newDescendant, hwnd) + else: + bridgeDll.releaseJavaObject(vmID, newDescendant) for accContext in [event, oldDescendant]: bridgeDll.releaseJavaObject(vmID, accContext) @@ -972,8 +1001,13 @@ def internal_hasFocus(sourceContext): @AccessBridge_PropertyNameChangeFP -def event_nameChange(vmID, event, source, oldVal, newVal): - jabContext = JABContext(vmID=vmID, accContext=source) +def internal_event_nameChange(vmID, event, source, oldVal, newVal): + internalQueueFunction(event_nameChange, vmID, source) + bridgeDll.releaseJavaObject(vmID, event) + + +def event_nameChange(vmID, accContext): + jabContext = JABContext(vmID=vmID, accContext=accContext) if jabContext.hwnd: focus = api.getFocusObject() obj = ( @@ -985,12 +1019,16 @@ def event_nameChange(vmID, event, source, oldVal, newVal): eventHandler.queueEvent("nameChange", obj) else: log.debugWarning("Unable to obtain window handle for accessible context") - bridgeDll.releaseJavaObject(vmID, event) @AccessBridge_PropertyDescriptionChangeFP -def event_descriptionChange(vmID, event, source, oldVal, newVal): - jabContext = JABContext(vmID=vmID, accContext=source) +def internal_event_descriptionChange(vmID, event, source, oldVal, newVal): + internalQueueFunction(event_descriptionChange, vmID, source) + bridgeDll.releaseJavaObject(vmID, event) + + +def event_descriptionChange(vmID, accContext): + jabContext = JABContext(vmID=vmID, accContext=accContext) if jabContext.hwnd: focus = api.getFocusObject() obj = ( @@ -1002,12 +1040,16 @@ def event_descriptionChange(vmID, event, source, oldVal, newVal): eventHandler.queueEvent("descriptionChange", obj) else: log.debugWarning("Unable to obtain window handle for accessible context") - bridgeDll.releaseJavaObject(vmID, event) @AccessBridge_PropertyValueChangeFP -def event_valueChange(vmID, event, source, oldVal, newVal): - jabContext = JABContext(vmID=vmID, accContext=source) +def internal_event_valueChange(vmID, event, source, oldVal, newVal): + internalQueueFunction(event_valueChange, vmID, source) + bridgeDll.releaseJavaObject(vmID, event) + + +def event_valueChange(vmID, accContext): + jabContext = JABContext(vmID=vmID, accContext=accContext) if jabContext.hwnd: focus = api.getFocusObject() obj = ( @@ -1019,7 +1061,6 @@ def event_valueChange(vmID, event, source, oldVal, newVal): eventHandler.queueEvent("valueChange", obj) else: log.debugWarning("Unable to obtain window handle for accessible context") - bridgeDll.releaseJavaObject(vmID, event) @AccessBridge_PropertyStateChangeFP @@ -1155,9 +1196,9 @@ def initialize(): # Register java events bridgeDll.setFocusGainedFP(internal_event_focusGained) bridgeDll.setPropertyActiveDescendentChangeFP(internal_event_activeDescendantChange) - bridgeDll.setPropertyNameChangeFP(event_nameChange) - bridgeDll.setPropertyDescriptionChangeFP(event_descriptionChange) - bridgeDll.setPropertyValueChangeFP(event_valueChange) + bridgeDll.setPropertyNameChangeFP(internal_event_nameChange) + bridgeDll.setPropertyDescriptionChangeFP(internal_event_descriptionChange) + bridgeDll.setPropertyValueChangeFP(internal_event_valueChange) bridgeDll.setPropertyStateChangeFP(internal_event_stateChange) bridgeDll.setPropertyCaretChangeFP(internal_event_caretChange) isRunning = True @@ -1174,6 +1215,9 @@ def terminate(): return bridgeDll.setFocusGainedFP(None) bridgeDll.setPropertyActiveDescendentChangeFP(None) + bridgeDll.setPropertyNameChangeFP(None) + bridgeDll.setPropertyDescriptionChangeFP(None) + bridgeDll.setPropertyValueChangeFP(None) bridgeDll.setPropertyStateChangeFP(None) bridgeDll.setPropertyCaretChangeFP(None) h = bridgeDll._handle diff --git a/source/NVDAObjects/JAB/__init__.py b/source/NVDAObjects/JAB/__init__.py index b84864d8c54..db9cb8b2e71 100644 --- a/source/NVDAObjects/JAB/__init__.py +++ b/source/NVDAObjects/JAB/__init__.py @@ -1,5 +1,5 @@ # A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2006-2025 NV Access Limited, Leonard de Ruijter, Joseph Lee, Renaud Paquay, pvagner, hwf1324 +# Copyright (C) 2006-2026 NV Access Limited, Leonard de Ruijter, Joseph Lee, Renaud Paquay, pvagner, hwf1324 # This file is covered by the GNU General Public License. # See the file COPYING for more details. @@ -248,13 +248,44 @@ def findOverlayClasses(self, clsList: list[NVDAObject]) -> None: clsList.append(ComboBox) elif role == "table": clsList.append(Table) - elif self.parent and isinstance(self.parent, Table) and self.parent._jabTableInfo: + elif self._hasTableParent(): clsList.append(TableCell) elif role == "progress bar": clsList.append(ProgressBar) clsList.append(JAB) + def _hasTableParent(self) -> bool: + """Lightweight check if the immediate parent is a Table. + + On the fast path (parent already cached), returns immediately + without any bridge calls. On the first call, checks the parent's + role via a lightweight bridge call and only creates a full NVDAObject + when the role is "table" (which involves additional bridge calls). + """ + if hasattr(self, "_parent"): + parent = self._parent + return parent is not None and isinstance(parent, Table) and parent._jabTableInfo + parentContext = self.jabContext.getAccessibleParentFromContext() + if not parentContext: + return False + try: + parentInfo = parentContext.getAccessibleContextInfo() + except RuntimeError: + log.debugWarning("Could not get accessible context info for parent", exc_info=True) + return False + if parentInfo.role_en_US != "table": + return False + if self.indexInParent is None: + # Without indexInParent we cannot construct a valid JAB parent; + # _get_parent would also fall back to the Window ancestor here. + return False + # Parent is a table — create the full parent object reusing the context + # we already hold, so _get_parent doesn't make a redundant bridge call. + self._parent = JAB(jabContext=parentContext) + parent = self._parent + return parent is not None and isinstance(parent, Table) and parent._jabTableInfo + @classmethod def kwargsFromSuper(cls, kwargs, relation: str | None = None) -> bool: jabContext = None diff --git a/user_docs/en/changes.md b/user_docs/en/changes.md index 93821b8bbd8..32f70e20ec6 100644 --- a/user_docs/en/changes.md +++ b/user_docs/en/changes.md @@ -58,6 +58,7 @@ This is more noticeable for Windows releases which are enablement packages on to * Configuration profile triggers now activate when the Add-on Store is open. (#19583, @bramd) * Decorative Unicode letters such as negative squared, negative circled, and regional indicator symbol characters are now normalized to their base Latin letters when Unicode normalization is enabled. (#19608, @bramd) * NVDA no longer crashes when the Add-on Store download directory cannot be cleaned up due to file permission errors. (#19202, @christopherpross) +* Fixed NVDA freezing when navigating in JetBrains IDEs. (#16741, @christopherpross) ### Changes for Developers