Este artículo pretende aportar un resumen del diseño técnico y la arquitectura de NVDA. Es, en cierto modo, necesariamente de naturaleza técnica. Deberías tener unos conocimientos aceptables en programación, y en particular en programación orientada a objetos, así como conocimientos básicos de Python, antes de pretender entender el diseño de NVDA. Por favor, mira la documentación del código para más información sobre las clases relevantes.
Terminología
Abreviaturas
- API: application programming interface, interfaz de programación de aplicaciones.
- GUI: graphical user interface, interfaz gráfica de usuario.
Definiciones
- Caret: el cursor del sistema; p. ej. el cursor que se mueve generalmente cuando usas las teclas normales del cursor.
- Script: una función que se ejecuta en respuesta a la entrada del usuario, tales como pulsación de teclas en el teclado, manipulación de los controles de una pantalla braille y toques en pantallas táctiles. También se conocen como órdenes.
- Widget: un componente individual en una GUI con el que un usuario puede interactuar; por ejemplo un botón, un campo de texto editable, un cuadro de lista, etc. También se conocen como controles u objetos.
General
Lenguajes de programación
NVDA está principalmente escrito en el lenguaje de programación Python, que permite un desarrollo muy rápido entre otras ventajas. El código que hay que inyectar en otros procesos está escrito en C++ para mejorar su rendimiento.
Apis de accesibilidad
Para hacer que los widgets gráficos sean accesibles para las tecnologías de asistencia, tanto el sistema operativo como las aplicaciones pueden usar apis específicas de accesibilidad. Estas apis ofrecen información sobre el widget, como por ejemplo su nombre, tipo o rol (botón, casilla de verificación, campo de texto editable, etc), descripción, valor, estados (marcado, no disponible, invisible, etc) y acceso directo de teclado. Las apis de accesibilidad también ofrecen eventos para permitir que las tecnologías de asistencia puedan reaccionar ante cambios, tales como de foco, modificación de propiedades de un objeto (nombre, descripción, valor y estado), etc. Las apis de accesibilidad enriquecidas ofrecen información adicional, incluyendo la capacidad para acceder a información extra y rastrear el cursor en controles de texto editable, e información de tablas tal como coordenadas de fila y columna. NVDA se apoya mayoritariamente en estas apis para obtener la información que necesita. Se usan varias apis de accesibilidad, incluyendo Microsoft Active Accessibility (MSAA) (también conocida como IAccessible), IAccessible2, Java Access Bridge y UI Automation.
Apis nativas
Algunos widgets no exponen información suficiente de accesibilidad a través de las apis anteriores, y por lo tanto no son accesibles. Por ejemplo MSAA, que es la api usada por la mayoría de controles nativos en Windows, no da la posibilidad de obtener la posición del cursor o unidades individuales de texto en campos de texto editable. Sin embargo, algunos widgets ofrecen sus propias apis nativas (no relacionadas con la accesibilidad) que pueden usarse para obtener esta información. NVDA hace uso de ellas cuando se le presenta la oportunidad, por ejemplo en cuadros de edición estándar.
Funciones del sistema operativo
Además de las apis nativas y aquellas específicas de accesibilidad, Windows ofrece muchas funciones que pueden usarse para obtener información y realizar tareas. Entre la información que se puede obtener se incluye el nombre de clase de una ventana, la ventana que hay actualmente en primer plano y el estado de la batería del ordenador. Entre las tareas que pueden realizarse se incluyen movimientos y clics del ratón, así como envío de pulsaciones de teclado.
Componentes de NVDA
NVDA se ha construido en base a un complejo diseño modular, abstracto y orientado a objetos. Está dividido en varios componentes distintos.
Lanzador
El lanzador es el módulo que el usuario ejecuta para iniciar NVDA. Se encuentra en el archivo nvda.pyw. Procesa los argumentos de línea de comandos, realiza algunas tareas de inicialización e inicia el núcleo (a no ser que NVDA ya se esté ejecutando o haya un argumento que indique lo contrario).
Núcleo
El núcleo (en la función core.main) carga la configuración, inicializa todos los demás componentes y entra en el bucle de ejecución principal.
El bucle principal se mantiene iterando hasta que NVDA recibe la orden de salir. En cada iteración, el núcleo llama a la api y los manejadores de entrada, generadores registrados y la cola principal. Todos los eventos, scripts, etc. se meten indirectamente en la cola principal a través de la api y los manejadores de entrada, por lo que se activan cada vez que se procesa esta cola. El bucle principal es el responsable de que NVDA funcione.
Una vez que NVDA recibe la orden de salir, el núcleo hace que el resto de componentes se desactiven en orden y de la forma adecuada, guarda la configuración si corresponde y después sale.
Manejo de scripts y eventos
En vez de meter los scripts y eventos directamente en la cola principal, se abstraen utilizando los módulos eventHandler y scriptHandler, respectivamente. Los manejadores de api y entrada usan estos módulos para meter en la cola o ejecutar directamente scripts y eventos.
Generadores registrados
En el caso de algunas tareas, es necesario que estas se ejecuten en segundo plano sin que NVDA se cuelgue o se bloquee esperando a que terminen. Necesitan ejecutar bloques de código regularmente, pero sin un intervalo específico de tiempo. NVDA permite registrar funciones generadoras de Python con este propósito. Una vez registrado, cada generador se ejecutará en cada iteración del bucle principal. Entre algunas de estas funciones podemos destacar las de verbalizar todo, o deletrear un texto. Se registran llamando a queueHandler.registerGeneratorObject.
Manejadores de entrada
Los manejadores de entrada procesan la entrada de órdenes desde distintas fuentes. Actualmente, hay tres módulos principales de manejo de entrada: keyboardHandler (teclado), mouseHandler (ratón) y touchHandler (pantallas táctiles).
Los controladores para pantallas braille también pueden procesar datos de entrada. Sus manejadores procesan la entrada recibida desde la pantalla braille y generan los gestos de entrada y los eventos apropiados.
Gestos de entrada
Un gesto de entrada es una representación abstracta de una parte única de entrada del usuario; por ejemplo, una pulsación de teclado. Todos los gestos de entrada heredan de la clase base inputCore.InputGesture. Esto permite manejar toda la entrada de una forma unificada y consistente. Por ejemplo, cualquier gesto de entrada se puede asociar a cualquier script, tanto por el usuario como desde el código.
Manejadores de api
Se encargan de controlar la inicialización y finalización de apis nativas y de accesibilidad, y escuchar los eventos producidos por ellas. También contienen funciones útiles para trabajar con la api que se encargan de gestionar. Cuando se recibe un evento para un widget, se recupera o se construye un objeto de NVDA y se pone en la cola un evento para ese objeto. Además de esto, también abstraen el manejo de las consultas y eventos de apis específicas, de tal forma que el resto de NVDA no tenga que programarse en función de una api concreta. Para añadir soporte para una nueva api, un desarrollador sólo tiene que crear un manejador para esa api y objetos de NVDA específicos, sin necesidad de cambiar la mayoría del código. Algunos módulos manejadores de apis son IAccessibleHandler para MSAA, IAccessible e IAccessible2, JABHandler para Java Access Bridge, y UIAHandler para UI Automation.
Módulos de salida
La funcionalidad de salida se encapsula en módulos separados. Actualmente hay dos módulos principales de salida: speech (voz) y braille. También está el módulo tones, que se usa para reproducir tonos y pitidos, y el módulo nvWave, usado para reproducir archivos de sonido en formato wav para ciertos eventos.
Controladores de salida
Los controladores de síntesis sirven para utilizar sintetizadores de voz específicos. Heredan de la clase base synthDriverHandler.SynthDriver.
Los controladores para pantallas braille permiten que NVDA se pueda usar con distintos modelos de pantallas braille. Heredan de la clase base braille.BrailleDisplayDriver.
Objetos de NVDA
Un objeto de NVDA (NVDAObject) es una representación abstracta de un widget en NVDA. Todos los objetos de NVDA heredan de la clase base NVDAObjects.NVDAObject. Sus métodos y propiedades se usan para recoger información, reaccionar ante eventos y ejecutar acciones en el widget representado de forma abstracta por el objeto. Esto significa que NVDA puede trabajar con representaciones únicas y abstractas, sin que tengamos que preocuparnos del código específico de la api que controla el widget. Esto permite una integración sencilla con muchísimas apis nativas y de accesibilidad.
Aquí es donde entra todo el poder de la programación orientada a objetos. Muchos métodos se implementan en la clase base, y sólo se sobreescriben en las clases hijas si deseamos añadir funciones extra o cambiar su comportamiento.
De la misma manera, si un widget particular no es estándar, problemático, ofrece información usando otros mecanismos, etc; simplemente hay que crear clases hija y sobreescribir los métodos apropiados de forma adecuada.
Los objetos de NVDA más comunes que pueden aparecer en cualquier aplicación se encuentran en el paquete NVDAObjects.
Los módulos de aplicación también pueden definir sus propios objetos específicos de una aplicación concreta.
Fragmentos de texto
Cuando trabajamos con controles de texto editable, NVDA necesita obtener información sobre el texto en el widget. Además de obtener el texto completo, para una navegación adecuada se deben poder recuperar unidades concretas de texto (párrafos, líneas, palabras o letras), así como obtener y establecer la posición del caret y la selección. Si el widget también soporta formato, NVDA debería ser capaz de recuperar atributos del texto como nombre de la fuente, tamaño, negrita, cursiva, subrayado y errores de ortografía. Cada api ofrece una forma distinta de consultar y manipular texto. Al igual que los objetos de NVDA ofrecen la representación abstracta de un widget, los objetos TextInfo ofrecen una manera abstracta de representar fragmentos de texto. Estos objetos heredan de la clase base textInfos.TextInfo.
Órdenes globales
El objeto de órdenes globales (globalCommands.GlobalCommands) contiene scripts globales integrados; es decir, pueden ejecutarse en cualquier parte. Por ejemplo, los scripts para revisar, verbalizar el objeto que tiene el foco o dar información de fecha y hora están ubicados en las órdenes globales.
Complementos
Las características de NVDA pueden extenderse con facilidad a través de complementos creados por terceros. Estos pueden añadir nuevos objetos para aplicaciones específicas, funciones globales o soporte para sintetizadores de voz y pantallas braille. Hay 3 tipos de complementos: appModules, GlobalPlugins y controladores. Estos últimos se dividen en sintetizadores de voz y controladores braille.
Módulos de aplicación
Generalmente, una aplicación estándar puede contener la mayoría de widgets soportados por NVDA, y estos tienen su objeto correspondiente definido en el paquete principal NVDAObjects. Sin embargo, hay casos donde una aplicación tiene un widget específico, o debe redefinirse un evento, o es necesario añadir un script sólo para esa aplicación. Un módulo de aplicación da soporte específico en esos casos.
Los módulos de aplicación heredan de la clase base appModuleHandler.AppModule. Los módulos de aplicación reciben los eventos generados por los objetos NVDA de esa aplicación, y pueden asociar scripts que se ejecuten en cualquier parte de la aplicación. Además, pueden implementar sus propios objetos NVDA para esa aplicación.
Complementos globales
Hacen lo mismo que los módulos de aplicación, pero a nivel global con NVDA. Por ejemplo se pueden añadir nuevas órdenes globales, modificar el comportamiento de NVDA o dar soporte a nuevos motores de interfaz gráfica. Esto puede hacerse usando complementos globales.
Un complemento global hereda de la clase base globalPluginHandler.GlobalPlugin. Al igual que en el caso de los módulos de aplicación, un complemento global puede agregar sus propias órdenes y objetos NVDA nuevos.
Interceptores de árbol
En ocasiones, es necesario interceptar eventos y scripts para jerarquías o árboles de objetos NVDA. Por ejemplo, esto se hace para procesar documentos complejos que constan de muchos objetos. Para ello se usa un interceptor de árbol.
Un interceptor de árbol (TreeInterceptor) hereda de la clase base treeInterceptorHandler.TreeInterceptor. Recibe eventos y scripts para todos los objetos NVDA que lo componen, incluyendo el objeto raíz. Los interceptores de árbol se crean cuando se devuelve una clase TreeInterceptor en la propiedad treeInterceptorClass de un objeto NVDA.
Buffers virtuales
Los documentos complejos tales como páginas web rara vez son planos; la información no va simplemente de arriba hacia abajo. Por este motivo, los exploradores de documentos complejos pocas veces dan la posibilidad de navegar por el documento con el caret y, cuando lo hacen, suele dar problemas. Por este motivo, los lectores de pantalla deben crear su propia representación plana del documento a partir de la jerarquía de objetos ofrecida por el navegador y permitir al usuario navegar por esta representación. NVDA llama a esto buffers virtuales. Debido al gran número de consultas que hay que hacer al navegador, NVDA crea estas representaciones usando código en el interior del proceso.
Un buffer virtual (VirtualBuffer) hereda de la clase base virtualBuffers.VirtualBuffer, y es un tipo de interceptor de árbol.
gui
NVDA tiene su propia interfaz gráfica para ayudar a que la interacción del usuario sea sencilla y cambiar aspectos de la configuración. El código está principalmente en el paquete gui.
Se usa el motor gráfico WXPython.
Gestión de la configuración
NVDA incluye un extenso sistema de configuración con varios diálogos de preferencias, posibilidad de aplicar ajustes en aplicaciones concretas, y mucho más. Las opciones básicas de configuración, así como el código que administra los perfiles y disparadores, se encuentran en el paquete config, y NVDA usa ConfigObj para almacenar los ajustes.
Funciones especiales de objetos
Eventos
Las instancias de módulos de aplicación, buffers virtuales y objetos de NVDA pueden contener métodos especiales que procesan eventos para objetos de NVDA. Estos eventos siempre se nombran comenzando con «event_». Por ejemplo, event_gainFocus o event_nameChange. Estos eventos generalmente se ejecutan al llamar a eventHandler.executeEvent, y esto suele hacerse cuando los manejadores de api introducen eventos en la cola. La mayoría de los eventos no reciben argumentos adicionales. Los buffers virtuales y los módulos de aplicación reciben una función manejadora que debería llamarse si el evento debe ser procesado por el siguiente manejador; por ejemplo, el objeto en sí mismo.
Scripts
Los objetos de NVDA, los módulos de aplicación y los buffers virtuales pueden contener métodos especiales llamados scripts, que se ejecutan en respuesta a gestos de entrada realizados por el usuario. Estos métodos se nombran empezando con el texto «script_»; por ejemplo, script_reportCurrentFocus o script_dateTime. Los scripts reciben el gesto de entrada que los invocó.
Los gestos de entrada se asocian a los scripts en cada clase usando un diccionario llamado __gestures. También pueden asociarse en tiempo de ejecución llamando a bindGesture. Heredan de la clase baseObject.ScriptableObject.
Comunicación interproceso
En términos generales, cada aplicación o servicio que se ejecuta en el ordenador, incluyendo NVDA, es un proceso. Ningún proceso puede acceder a datos de otro proceso, a no ser que utilice mecanismos especiales del sistema operativo. Esto se llama comunicación interproceso (IPC).
Código fuera del proceso
NVDA funciona principalmente fuera de los procesos. Esto significa que la mayoría de los eventos y las consultas de información se realizan utilizando IPC entre NVDA y el proceso en cuestión.
Esto es infinitamente más lento que gestionar las consultas y eventos dentro del propio proceso. Por suerte, para la mayoría de funciones de un lector de pantalla, y con las tecnologías hardware actuales, las pérdidas de rendimiento son insignificantes.
Código dentro del proceso
Cuando necesitamos llevar a cabo un gran número de consultas de golpe, trabajar fuera del proceso es increíblemente lento. Un ejemplo es el renderizado de una página web para convertirla en una representación plana, tal y como hacen los buffers virtuales de los que hemos hablado más arriba. En estos casos, se «inyecta» código en el proceso remoto. Como el código se ejecuta dentro del mismo proceso, las consultas y eventos se procesan con mayor rapidez, ya que no tienen que viajar continuamente de un proceso a otro. NVDA puede obtener la información que necesita al final, de una forma sencilla y eficiente.
El código inyectado tiene que ser ligero y rápido, para garantizar un buen rendimiento en el proceso afectado. Python no es adecuado para esta tarea, por lo que el código inyectable se programa en C++. De esta forma, se garantiza el máximo rendimiento y la mínima sobrecarga.