Skip to main content
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.

How Events Work

When NVDA detects accessibility events from the operating system or applications, it:
  1. Receives the event from the underlying API (IAccessible, UIA, etc.)
  2. Abstracts the event into NVDA’s internal format
  3. Propagates the event through a chain of handlers
  4. Processes the event to update speech, braille, and UI

Event Propagation Chain

Events propagate through handlers in this order:
1

Global Plugins

All loaded global plugins receive the event first
2

App Module

The app module for the object’s application
3

Tree Interceptor

The tree interceptor (like browse mode) if applicable
4

NVDA Object

The NVDA Object itself that triggered the event

Event Method Signatures

On NVDA Objects

When defined directly on an NVDA Object:
Event on NVDA Object
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()

On Plugins and App Modules

When defined on global plugins, app modules, or tree interceptors:
Event on plugin
import globalPluginHandler

class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    def event_gainFocus(self, obj, nextHandler):
        """Handle focus events globally"""
        # obj: the NVDA Object that gained focus
        # nextHandler: function to call to propagate event
        
        # Do custom processing
        if obj.role == controlTypes.Role.BUTTON:
            import tones
            tones.beep(440, 50)
        
        # Always call nextHandler to continue propagation
        nextHandler()
Always call nextHandler() unless you specifically want to stop event propagation. Failing to do so can break NVDA’s normal event processing.

Common Events

Focus Events

Focus events
import appModuleHandler
import ui

class 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()

Property Change Events

Property change events
    def event_nameChange(self, obj, nextHandler):
        """Object's name changed"""
        ui.message(f"Name changed to {obj.name}")
        nextHandler()
    
    def event_valueChange(self, obj, nextHandler):
        """Object's value changed"""
        # Common for sliders, progress bars, edit fields
        if obj.role == controlTypes.Role.SLIDER:
            ui.message(f"{obj.value}")
        nextHandler()
    
    def event_stateChange(self, obj, nextHandler):
        """Object's state changed"""
        # Check specific state changes
        if controlTypes.State.CHECKED in obj.states:
            ui.message("Checked")
        nextHandler()
    
    def event_descriptionChange(self, obj, nextHandler):
        """Object's description changed"""
        nextHandler()

Text and Caret Events

Text and caret events
    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()

Window Events

Window events
    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()

Selection Events

Selection events
    def event_selection(self, obj, nextHandler):
        """Item was selected"""
        # Common in lists, trees, combo boxes
        if obj.role == controlTypes.Role.LISTITEM:
            ui.message(f"Selected {obj.name}")
        nextHandler()
    
    def event_selectionAdd(self, obj, nextHandler):
        """Item added to selection (multi-select)"""
        nextHandler()
    
    def event_selectionRemove(self, obj, nextHandler):
        """Item removed from selection"""
        nextHandler()
    
    def event_selectionWithin(self, obj, nextHandler):
        """Selection moved within container"""
        nextHandler()

Document Events

Document events
    def event_documentLoadComplete(self, obj, nextHandler):
        """Document finished loading"""
        # Common for web pages
        ui.message("Page loaded")
        nextHandler()
    
    def event_pageChange(self, obj, nextHandler):
        """Page changed in document"""
        nextHandler()

Application Events

Application events
    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

Real-World Examples

Example 1: Beep on Focus Changes

From the NVDA Developer Guide:
Notepad beeps
# Notepad App Module for NVDA
# Developer guide example 1

import appModuleHandler

class AppModule(appModuleHandler.AppModule):
    
    def event_gainFocus(self, obj, nextHandler):
        import tones
        tones.beep(550, 50)
        nextHandler()

Example 2: Announce Button Presses

Announce button activation
import appModuleHandler
import controlTypes
import ui

class AppModule(appModuleHandler.AppModule):
    
    def event_gainFocus(self, obj, nextHandler):
        # Announce when buttons are focused
        if obj.role == controlTypes.Role.BUTTON:
            ui.message(f"Button: {obj.name}")
        nextHandler()

Example 3: Monitor Value Changes

Monitor progress bars
import globalPluginHandler
import controlTypes
import ui

class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    
    def event_valueChange(self, obj, nextHandler):
        # Announce progress bar changes
        if obj.role == controlTypes.Role.PROGRESSBAR:
            try:
                value = int(obj.value.rstrip('%'))
                # Only announce every 10%
                if value % 10 == 0:
                    ui.message(f"{value}%")
            except:
                pass
        nextHandler()

Example 4: Track Focus Entering Dialogs

Dialog tracking
import appModuleHandler
import controlTypes
import tones

class AppModule(appModuleHandler.AppModule):
    
    def event_focusEntered(self, obj, nextHandler):
        # Play sound when entering a dialog
        if obj.role == controlTypes.Role.DIALOG:
            tones.beep(880, 100)
        nextHandler()

Example 5: Live Region Monitoring

Live region monitoring
import globalPluginHandler
import ui

class 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()

Event Timing and Threading

Events are processed on NVDA’s main thread:
Event timing
import queueHandler
import time

class 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.

Stopping Event Propagation

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.

Event Filtering

Filter events before processing:
Event filtering
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()

Custom Events

You can fire custom events:
Custom events
import eventHandler

# Fire a custom event
eventHandler.queueEvent("customEvent", obj)

# Handle custom event
class GlobalPlugin(globalPluginHandler.GlobalPlugin):
    def event_customEvent(self, obj, nextHandler):
        ui.message("Custom event received")
        nextHandler()

Event Queuing

Events are queued to prevent flooding:
Event queuing
import eventHandler

# Queue an event
eventHandler.queueEvent("gainFocus", obj)

# Events are processed in order on the event queue
# Multiple identical events may be coalesced

Debugging Events

Log events for debugging:
Event debugging
import appModuleHandler
from logHandler import log

class AppModule(appModuleHandler.AppModule):
    
    def event_gainFocus(self, obj, nextHandler):
        # Log event details
        log.debug(
            f"Focus event: {obj.name}, "
            f"role={obj.role}, "
            f"class={obj.windowClassName}"
        )
        nextHandler()
Enable debug logging in NVDA settings to see detailed event information: NVDA menu → Tools → Reload plugins

Performance Considerations

Event handlers should be fast:
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()
Use properties with caching:
class MyObject(NVDAObject):
    _cache_expensiveProperty = True
    
    def _get_expensiveProperty(self):
        # Cached for the core pump cycle
        return self._computeExpensive()
Throttle high-frequency events:
import time

class 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()

Best Practices

  1. Always call nextHandler() unless you’re intentionally stopping propagation
  2. Keep handlers fast - use background threads for expensive operations
  3. Filter early - check conditions before doing work
  4. Use appropriate event types - don’t handle all events when you need specific ones
  5. Log for debugging - use log.debug() to trace event flow
  6. Test thoroughly - events fire frequently, ensure your code is efficient

See Also

NVDA Objects

Learn about the objects that fire events

Scripts & Gestures

Handle user input with scripts

Writing Plugins

Create plugins that handle events

Architecture Overview

Understand the bigger picture

Build docs developers (and LLMs) love