Overview
Global Plugins allow you to add functionality that works across all applications. Unlike app modules which are specific to one application, global plugins are loaded when NVDA starts and remain active until NVDA exits.
Use global plugins for features that should be available everywhere, such as:
New navigation commands
System-wide information scripts
Custom braille features
OCR functionality
Cloud synchronization
Basic Global Plugin Structure
Global plugins must:
Be placed in the globalPlugins directory
Have a unique filename that describes their purpose
Define a class called GlobalPlugin that inherits from globalPluginHandler.GlobalPlugin
Basic Plugin
globalPluginHandler.py
# timeAnnouncement.py
# Global Plugin for enhanced time announcements
import globalPluginHandler
from scriptHandler import script
import ui
import datetime
class GlobalPlugin ( globalPluginHandler . GlobalPlugin ):
@script (
description = "Announces the current time with seconds" ,
gesture = "kb:NVDA+f12"
)
def script_announceTimeDetailed ( self , gesture ):
"""Announce time including seconds."""
now = datetime.datetime.now()
ui.message(now.strftime( "%I:%M:%S %p" ))
Lifecycle Methods
Global plugins have two key lifecycle methods:
Called when the plugin is loaded (NVDA startup). def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
# Initialize your plugin
self .myData = {}
Called when the plugin is being unloaded (NVDA exit). def terminate ( self ):
# Clean up resources
super ().terminate()
Handling Events
Global plugins receive events from all applications and controls. Event methods are named event_eventName and take three arguments:
import globalPluginHandler
import ui
import controlTypes
class GlobalPlugin ( globalPluginHandler . GlobalPlugin ):
def event_gainFocus ( self , obj , nextHandler ):
"""Called when any object gains focus."""
# Check if this is a specific type of control
if obj.role == controlTypes.Role. BUTTON :
# Do custom processing for buttons
pass
# Always call nextHandler to propagate the event
nextHandler()
def event_nameChange ( self , obj , nextHandler ):
"""Called when any object's name changes."""
if obj.windowClassName == "SpecialControl" :
ui.message( f "Name changed to: { obj.name } " )
nextHandler()
def event_foreground ( self , obj , nextHandler ):
"""Called when the foreground application changes."""
# Track application switches
nextHandler()
Event Method Signature
The NVDA Object on which the event occurred
Function to call to propagate the event to the next handler. Always call this unless you have a specific reason not to.
Creating Scripts
Scripts are commands bound to keyboard shortcuts that users can execute from anywhere:
Basic Scripts
Advanced Scripts
import globalPluginHandler
from scriptHandler import script
import ui
import api
class GlobalPlugin ( globalPluginHandler . GlobalPlugin ):
@script (
description = "Announces current battery status" ,
category = "System Information" ,
gesture = "kb:NVDA+shift+b"
)
def script_announceBattery ( self , gesture ):
"""Announce battery status."""
# Get battery information
ui.message( "Battery: 80 % r emaining" )
@script (
description = "Speaks window title and class" ,
gestures = [ "kb:NVDA+alt+t" , "kb:NVDA+shift+t" ]
)
def script_windowInfo ( self , gesture ):
"""Announce window information."""
fg = api.getForegroundObject()
ui.message( f " { fg.name } , class: { fg.windowClassName } " )
Script Decorator Parameters
User-visible description shown in the Input Gestures dialog
Category for grouping in Input Gestures (uses plugin’s scriptCategory if not specified)
Single keyboard gesture (e.g., "kb:NVDA+shift+v")
Multiple keyboard gestures
Whether the script applies to focus ancestor objects
Run the script even when input help is active
Allow script execution when sleep mode is active
The say all mode to resume after executing the script (from speech.sayAll.CURSOR)
Produce speech when called while speech mode is “on-demand”
Gestures identify input that triggers scripts:
Keyboard Gestures
Braille Gestures
Other Input
# Standard keyboard
"kb:NVDA+shift+v"
"kb:control+alt+delete"
"kb:f1"
# Laptop layout
"kb(laptop):NVDA+t"
"kb(laptop):NVDA+shift+upArrow"
# Named keys
"kb:escape"
"kb:enter"
"kb:space"
"kb:tab"
"kb:backspace"
"kb:delete"
"kb:home"
"kb:end"
"kb:pageUp"
"kb:pageDown"
"kb:upArrow"
"kb:downArrow"
"kb:leftArrow"
"kb:rightArrow"
Customizing NVDA Objects
Global plugins can customize NVDA Objects system-wide:
import globalPluginHandler
from NVDAObjects.IAccessible import IAccessible
import controlTypes
class GlobalPlugin ( globalPluginHandler . GlobalPlugin ):
def chooseNVDAObjectOverlayClasses ( self , obj , clsList ):
"""Choose overlay classes for any NVDA Object."""
# Add custom behavior to all edit fields
if obj.role == controlTypes.Role. EDITABLETEXT :
clsList.insert( 0 , EnhancedEditField)
# Add custom behavior to specific window classes
if obj.windowClassName == "CustomWidget" :
clsList.insert( 0 , CustomWidget)
class EnhancedEditField ( IAccessible ):
"""Enhanced edit field with additional features."""
def _get_description ( self ):
"""Add helpful description."""
return super ().description or "Edit field"
def event_gainFocus ( self ):
"""Custom focus handling."""
super ().event_gainFocus()
# Additional announcements
class CustomWidget ( IAccessible ):
"""Custom widget support."""
def _get_name ( self ):
"""Better name extraction."""
# Custom logic
return "Custom widget"
Practical Examples
Example 1: Application Switcher
# Enhanced application switching announcements
import globalPluginHandler
import ui
class GlobalPlugin ( globalPluginHandler . GlobalPlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .lastApp = None
def event_foreground ( self , obj , nextHandler ):
"""Announce application switches with detail."""
appModule = obj.appModule
if appModule and appModule.appName != self .lastApp:
self .lastApp = appModule.appName
# Announce with more detail
ui.message(
f "Switched to { obj.name } , { appModule.productName } "
)
nextHandler()
Example 2: Clipboard Monitor
# Monitor and enhance clipboard operations
import globalPluginHandler
from scriptHandler import script
import api
import ui
import threading
import time
class GlobalPlugin ( globalPluginHandler . GlobalPlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
self .clipboardHistory = []
self .monitoringEnabled = False
@script (
description = "Toggle clipboard monitoring" ,
category = "Clipboard" ,
gesture = "kb:NVDA+shift+c"
)
def script_toggleMonitoring ( self , gesture ):
"""Toggle clipboard monitoring."""
self .monitoringEnabled = not self .monitoringEnabled
state = "enabled" if self .monitoringEnabled else "disabled"
ui.message( f "Clipboard monitoring { state } " )
@script (
description = "Announce clipboard content" ,
gesture = "kb:NVDA+c"
)
def script_announceClipboard ( self , gesture ):
"""Announce what's on the clipboard."""
text = api.getClipData()
if text:
ui.message( f "Clipboard: { text } " )
else :
ui.message( "Clipboard is empty" )
def terminate ( self ):
"""Clean up."""
self .monitoringEnabled = False
super ().terminate()
Example 3: OCR Integration
# Global OCR functionality
import globalPluginHandler
from scriptHandler import script
import ui
import api
class GlobalPlugin ( globalPluginHandler . GlobalPlugin ):
scriptCategory = "OCR"
@script (
description = "Recognize text in current navigator object" ,
gesture = "kb:NVDA+r"
)
def script_recognizeNavigator ( self , gesture ):
"""Perform OCR on navigator object."""
navigator = api.getNavigatorObject()
if not navigator:
ui.message( "No navigator object" )
return
try :
location = navigator.location
if not location:
ui.message( "Navigator object has no location" )
return
ui.message( "Recognizing..." )
# Perform OCR (simplified)
result = self ._performOCR(location)
ui.message(result)
except Exception as e:
ui.message( f "OCR failed: { e } " )
def _performOCR ( self , location ):
"""Perform OCR on screen region."""
# Implementation would use actual OCR library
return "Sample OCR result"
Example 4: Quick Settings
# Quick access to NVDA settings
import globalPluginHandler
from scriptHandler import script
import config
import ui
import speech
class GlobalPlugin ( globalPluginHandler . GlobalPlugin ):
scriptCategory = "Quick Settings"
@script (
description = "Cycle through speech rates" ,
gesture = "kb:NVDA+control+r"
)
def script_cycleRate ( self , gesture ):
"""Cycle through preset speech rates."""
rates = [ 25 , 50 , 75 , 100 ]
currentRate = config.conf[ "speech" ][ "synth" ][ "rate" ]
# Find next rate
try :
idx = rates.index(currentRate)
nextRate = rates[(idx + 1 ) % len (rates)]
except ValueError :
nextRate = rates[ 0 ]
config.conf[ "speech" ][ "synth" ][ "rate" ] = nextRate
speech.setSpeechRate(nextRate)
ui.message( f "Speech rate: { nextRate } " )
@script (
description = "Toggle capital pitch changes" ,
gesture = "kb:NVDA+control+p"
)
def script_toggleCapPitch ( self , gesture ):
"""Toggle capital letter pitch changes."""
current = config.conf[ "speech" ][ "capPitchChange" ]
config.conf[ "speech" ][ "capPitchChange" ] = not current
state = "enabled" if not current else "disabled"
ui.message( f "Capital pitch { state } " )
Configuration Management
Global plugins can save and load settings:
import globalPluginHandler
import config
from scriptHandler import script
import ui
class GlobalPlugin ( globalPluginHandler . GlobalPlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
# Add configuration spec
config.conf.spec[ "myPlugin" ] = {
"enabled" : "boolean(default=True)" ,
"announceLevel" : "integer(default=1, min=0, max=3)" ,
"soundEnabled" : "boolean(default=False)"
}
# Load settings
self .loadConfig()
def loadConfig ( self ):
"""Load plugin configuration."""
conf = config.conf[ "myPlugin" ]
self .enabled = conf[ "enabled" ]
self .announceLevel = conf[ "announceLevel" ]
self .soundEnabled = conf[ "soundEnabled" ]
def saveConfig ( self ):
"""Save plugin configuration."""
conf = config.conf[ "myPlugin" ]
conf[ "enabled" ] = self .enabled
conf[ "announceLevel" ] = self .announceLevel
conf[ "soundEnabled" ] = self .soundEnabled
@script ( description = "Toggle plugin" )
def script_toggle ( self , gesture ):
"""Toggle plugin on/off."""
self .enabled = not self .enabled
self .saveConfig()
ui.message( f "Plugin { 'enabled' if self .enabled else 'disabled' } " )
def terminate ( self ):
"""Save config on exit."""
self .saveConfig()
super ().terminate()
Threading and Timers
Be careful when using threads. NVDA is not fully thread-safe. Use wx.CallAfter or queueHandler.queueFunction to execute code on the main thread.
import globalPluginHandler
import threading
import wx
import queueHandler
import ui
class GlobalPlugin ( globalPluginHandler . GlobalPlugin ):
def __init__ ( self , * args , ** kwargs ):
super (). __init__ ( * args, ** kwargs)
# Start background task
self .thread = threading.Thread( target = self ._backgroundTask)
self .thread.daemon = True
self .running = True
self .thread.start()
def _backgroundTask ( self ):
"""Background task running in separate thread."""
while self .running:
# Do background work
threading.Event().wait( 5 ) # Wait 5 seconds
# Queue UI update on main thread
queueHandler.queueFunction(
queueHandler.eventQueue,
ui.message,
"Background task update"
)
def terminate ( self ):
"""Stop background task."""
self .running = False
if self .thread.is_alive():
self .thread.join( timeout = 1.0 )
super ().terminate()
Testing
Enable scratchpad
Go to NVDA Settings > Advanced and enable the scratchpad directory.
Create plugin
Place your global plugin in %APPDATA%\nvda\scratchpad\globalPlugins\.
Restart or reload
Either restart NVDA or press NVDA+Control+F3 to reload plugins.
Test functionality
Test your scripts and event handlers in various applications.
Check logs
View NVDA log (NVDA+F1) for any errors or debug messages.
Best Practices
Performance Considerations:
Avoid heavy processing in event handlers
Use threading for long-running operations
Always call nextHandler() unless you have a specific reason not to
Clean up resources in terminate()
Code Quality:
Provide clear script descriptions
Use appropriate script categories
Log debug information appropriately
Handle exceptions gracefully
Test in different NVDA configurations
Consider internationalization from the start
Common Pitfalls
Forgetting to call nextHandler()
# Wrong
def event_gainFocus ( self , obj , nextHandler ):
ui.message(obj.name)
# Missing nextHandler() - breaks event chain!
# Correct
def event_gainFocus ( self , obj , nextHandler ):
ui.message(obj.name)
nextHandler() # Always call this
Not cleaning up in terminate()
# Wrong
class GlobalPlugin ( globalPluginHandler . GlobalPlugin ):
def __init__ ( self ):
self .file = open ( "data.txt" , "w" )
# File never closed!
# Correct
class GlobalPlugin ( globalPluginHandler . GlobalPlugin ):
def __init__ ( self ):
self .file = open ( "data.txt" , "w" )
def terminate ( self ):
self .file.close()
super ().terminate()
Blocking the main thread
# Wrong - blocks NVDA
def script_download ( self , gesture ):
data = requests.get( "http://example.com" ) # Blocking!
ui.message(data)
# Correct - use threading
def script_download ( self , gesture ):
threading.Thread( target = self ._download).start()
def _download ( self ):
data = requests.get( "http://example.com" )
queueHandler.queueFunction(
queueHandler.eventQueue,
ui.message,
data
)