NVDA’s event system allows plugins and NVDA Objects to respond to changes in the GUI, such as focus changes, value updates, and state changes. Events are abstracted from various accessibility APIs into a unified system.
class MyNVDAObject(NVDAObject): def event_gainFocus(self): """Handle focus event on this object""" # self is the NVDA Object that gained focus import tones tones.beep(440, 50) # Must call parent to continue normal processing super().event_gainFocus()
import appModuleHandlerimport uiclass AppModule(appModuleHandler.AppModule): def event_gainFocus(self, obj, nextHandler): """Object gained focus""" ui.message(f"{obj.name} focused") nextHandler() def event_loseFocus(self, obj, nextHandler): """Object lost focus""" # Usually less common to handle nextHandler() def event_focusEntered(self, obj, nextHandler): """Focus entered a container Fired when focus moves inside an ancestor object. For example, when tabbing into a dialog. """ if obj.role == controlTypes.Role.DIALOG: ui.message(f"Entered {obj.name} dialog") nextHandler()
def event_caret(self, obj, nextHandler): """Caret moved within object This is the insertion point in text fields. """ # Get caret position try: info = obj.makeTextInfo(textInfos.POSITION_CARET) info.expand(textInfos.UNIT_CHARACTER) ui.message(info.text) except: pass nextHandler() def event_textChange(self, obj, nextHandler): """Text content changed""" # Useful for monitoring live regions nextHandler()
def event_foreground(self, obj, nextHandler): """Window came to foreground Fired when a window becomes the active top-level window. """ ui.message(f"{obj.name} window") nextHandler() def event_locationChange(self, obj, nextHandler): """Object moved on screen""" # Rarely needed, mostly for tracking window movement nextHandler()
def event_appModule_gainFocus(self): """This application gained focus Special event only on app modules. Called when switching to this application. """ ui.message("Application activated") def event_appModule_loseFocus(self): """This application lost focus Called when switching away from this application. """ pass
import globalPluginHandlerimport uiclass GlobalPlugin(globalPluginHandler.GlobalPlugin): def __init__(self): super().__init__() self._lastLiveText = {} def event_liveRegionChange(self, obj, nextHandler): """Handle live region updates""" # Get text info try: info = obj.makeTextInfo(textInfos.POSITION_ALL) text = info.text # Only announce if different from last time objId = id(obj) if text != self._lastLiveText.get(objId): ui.message(text) self._lastLiveText[objId] = text except: pass nextHandler()
import queueHandlerimport timeclass GlobalPlugin(globalPluginHandler.GlobalPlugin): def event_gainFocus(self, obj, nextHandler): # Events execute on main thread # Don't do expensive operations here # For long operations, use background thread def backgroundWork(): time.sleep(2) # Long operation # Queue result back to main thread queueHandler.queueFunction( queueHandler.eventQueue, ui.message, "Work complete" ) import threading threading.Thread(target=backgroundWork).start() nextHandler()
Do not perform long-running operations in event handlers as this will freeze NVDA. Use background threads for expensive operations.
Sometimes you want to stop an event from propagating further:
Stopping propagation
class AppModule(appModuleHandler.AppModule): def event_gainFocus(self, obj, nextHandler): # Check if we want to suppress this event if obj.role == controlTypes.Role.UNKNOWN: # Don't call nextHandler - stops propagation # Event won't reach NVDA Object or core return # Normal processing nextHandler()
Stopping propagation prevents NVDA’s core event processing. Only do this if you’re replacing NVDA’s default behavior entirely.
class AppModule(appModuleHandler.AppModule): def event_valueChange(self, obj, nextHandler): # Ignore value changes from progress bars in background from api import getFocusObject focus = getFocusObject() if obj.role == controlTypes.Role.PROGRESSBAR: # Only announce if it's in the focus ancestry import api if obj not in api.getFocusAncestors() and obj != focus: nextHandler() return # Process normally nextHandler()
import eventHandler# Queue an eventeventHandler.queueEvent("gainFocus", obj)# Events are processed in order on the event queue# Multiple identical events may be coalesced
def event_gainFocus(self, obj, nextHandler): # Bad - expensive operation # all_text = obj.makeTextInfo(textInfos.POSITION_ALL).text # Good - just access properties name = obj.name nextHandler()
Cache Expensive Computations
Use properties with caching:
class MyObject(NVDAObject): _cache_expensiveProperty = True def _get_expensiveProperty(self): # Cached for the core pump cycle return self._computeExpensive()
Limit Event Frequency
Throttle high-frequency events:
import timeclass GlobalPlugin(globalPluginHandler.GlobalPlugin): def __init__(self): super().__init__() self._lastValueChangeTime = 0 def event_valueChange(self, obj, nextHandler): now = time.time() # Only process every 100ms if now - self._lastValueChangeTime < 0.1: nextHandler() return self._lastValueChangeTime = now # Process event nextHandler()