Skip to content

Commit

Permalink
Release version 0.6
Browse files Browse the repository at this point in the history
  • Loading branch information
mehranlatifi83 committed Jan 15, 2025
1 parent 5db5de2 commit 9b65818
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 46 deletions.
29 changes: 29 additions & 0 deletions 0.6.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"addonId": "v2rayN",
"displayName": "v2rayN",
"URL": "",
"description": "Accessibility improvements for the v2rayN application",
"sha256": "16ff13ffa4531573d5c0631fcc7721d60f1e85cb8a56e2f716115206fb102a82",
"homepage": "https://github.com/mehranlatifi83/v2rayN",
"addonVersionName": "0.6",
"addonVersionNumber": {
"major": 0,
"minor": 6,
"patch": 0
},
"minNVDAVersion": {
"major": 2023,
"minor": 1,
"patch": 0
},
"lastTestedVersion": {
"major": 2024,
"minor": 4,
"patch": 1
},
"channel": "stable",
"publisher": "",
"sourceURL": "https://github.com/mehranlatifi83/v2rayN",
"license": "GPL 2",
"licenseURL": "https://www.gnu.org/licenses/gpl-2.0.html"
}
191 changes: 146 additions & 45 deletions addon/appModules/v2rayn.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# -*- coding:utf-8 -*-
import appModuleHandler
import addonHandler
import keyboardHandler
from NVDAObjects import NVDAObject
from controlTypes import Role, State
import api
Expand All @@ -22,22 +23,23 @@ def event_gainFocus(self):
This method is triggered when a window element gains focus.
It adjusts the focus to specific child elements for better navigation.
"""
if self.name.startswith('v2rayN') and self.children and self.children[1].role == Role.TOOLBAR and self.firstChild.role != Role.WINDOW:
if self.name and self.name.startswith('v2rayN') and self.children and len(self.children) >= 2 and self.children[1].role == Role.TOOLBAR and self.firstChild.role != Role.WINDOW:
# Set focus to a specific child element in v2rayN application
try:
self.children[1].lastChild.setFocus()
except Exception as e:
api.getForegroundObject().children[1].lastChild.setFocus()
elif (not self.name and len(self.children) == 1
and self.firstChild.firstChild.firstChild.firstChild
and self.firstChild.firstChild
and self.firstChild.firstChild.firstChild
and self.firstChild.firstChild.firstChild.UIAAutomationId == 'menuSystemProxyClear'):
# Focus on the designated child element in the application window
self.firstChild.firstChild.firstChild.setFocus()


class Toggle(UIA):
def event_stateChange(self):
if self.UIAAutomationId == 'PART_Toggle':
if self.UIAAutomationId and self.UIAAutomationId == 'PART_Toggle':
api.getForegroundObject(
).firstChild.firstChild.children[1].setFocus()

Expand All @@ -48,11 +50,6 @@ class AppModule(appModuleHandler.AppModule):
for enhanced accessibility.
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.isFocusedOnClose = False
self.isFocusedOnEnableTon = False

def maximize_window(self, hwnd):
"""
Change window mode to Maximize using Handle
Expand All @@ -64,27 +61,30 @@ def event_NVDAObject_init(self, obj):
"""
Adjust the name of UI elements based on their role to improve the screen reader's output.
"""
if obj.role == Role.MENUITEM:
if obj.role and obj.role == Role.MENUITEM:
# Combine the menu item's first child's name (if available) with its role text
name = obj.firstChild.name if not obj.name and obj.firstChild else obj.name
name = obj.firstChild.name if not obj.name and obj.firstChild and obj.firstChild.name else obj.name
role_text = obj.roleText or "Menu item"
obj.name = f"{name} {role_text}"
elif obj.role == Role.TOGGLEBUTTON:
elif obj.role and obj.role == Role.TOGGLEBUTTON:
# Use the previous static text element or automation ID for toggle button naming
name = (
obj.previous.name if not obj.name and obj.previous and obj.previous.role == Role.STATICTEXT
obj.previous.name if not obj.name and obj.previous and obj.previous.role and obj.previous.role == Role.STATICTEXT
else obj.UIAAutomationId if not obj.name and obj.UIAAutomationId
else obj.name
)
if obj.UIAAutomationId == 'PART_Toggle':
name = _("Settings")
obj.name = name
elif obj.role == Role.BUTTON:
elif obj.role and obj.role == Role.BUTTON:
# Use the name of the first child element for buttons if no name is defined
name = obj.firstChild.name if not obj.name and obj.firstChild else obj.UIAAutomationId[
5:] if obj.UIAAutomationId and not obj.name else obj.name
obj.name = name
elif obj.role == Role.COMBOBOX:
elif obj.role and obj.role == Role.LINK:
name = obj.firstChild.name if obj.firstChild and obj.firstChild.role and obj.firstChild.role == Role.STATICTEXT and obj.firstChild.name else obj.name
obj.name = name
elif obj.role and obj.role == Role.COMBOBOX:
# Set combo box name using the previous element's name or adjusted automation ID
name = (
obj.previous.name if not obj.name and obj.previous and obj.previous.role == Role.STATICTEXT
Expand All @@ -94,10 +94,10 @@ def event_NVDAObject_init(self, obj):
obj.name = name
# Remove default prefix from the value if it starts with "ServiceLib."
obj.value = (
"" if obj.value.startswith('ServiceLib.')
"" if obj.value and obj.value.startswith('ServiceLib.')
else obj.value
)
elif obj.role == Role.EDITABLETEXT:
elif obj.role and obj.role == Role.EDITABLETEXT:
# Use names of adjacent static text elements for editable text fields if available
name = (
obj.previous.name if not obj.name and obj.previous and obj.previous.role == Role.STATICTEXT and obj.previous.name
Expand All @@ -108,7 +108,7 @@ def event_NVDAObject_init(self, obj):
if obj.UIAAutomationId == "txtServerFilter":
name = obj.children[0].name
obj.name = name
elif obj.role == Role.LISTITEM:
elif obj.role and obj.role == Role.LISTITEM:
# Adjust name for list items
name = (
obj.firstChild.name if obj.firstChild and obj.firstChild.role == Role.STATICTEXT and obj.firstChild.name
Expand All @@ -120,93 +120,194 @@ def event_NVDAObject_init(self, obj):
name[name.index('(') + 1:-1] if name.startswith('V3-') and name.endswith(')')
else name
)
elif obj.role == Role.RADIOBUTTON:
elif obj.role and obj.role == Role.RADIOBUTTON:
# Adjust name for radio buttons
name = (
obj.firstChild.name if (not obj.name or obj.name.startswith("ServiceLib.")) and obj.firstChild and obj.firstChild.role == Role.STATICTEXT and obj.firstChild.name
else obj.name
)
obj.name = name
elif obj.role == Role.DATAITEM:
if obj.name.startswith("ServiceLib."):
elif obj.role and obj.role == Role.DATAITEM:
if obj.name and obj.name.startswith("ServiceLib."):
obj.name = ""
elif obj.role == Role.UNKNOWN:
if obj.name.startswith("Item: ServiceLib."):
if obj.name and obj.name.startswith("Item: ServiceLib."):
obj.name = ""
obj.value = "" if obj.name == obj.value else obj.value
obj.value = "" if obj.name and obj.value and obj.name == obj.value else obj.value
obj.name = f"{obj.name}, "

def chooseNVDAObjectOverlayClasses(self, obj, clsList):
"""
Add custom overlay classes for NVDA objects.
If the object is a window, insert the custom Window class.
"""
if isinstance(obj, UIA):
if obj.role == Role.WINDOW:
if obj.role and obj.role == Role.WINDOW:
clsList.insert(0, Window)
elif obj.role == Role.TOGGLEBUTTON:
elif obj.role and obj.role == Role.TOGGLEBUTTON:
clsList.insert(0, Toggle)

def event_focusEntered(self, obj: NVDAObject, nextHandler):
if obj.role == Role.DATAGRID:
if obj.role and obj.role == Role.DATAGRID:
message(f"{obj.UIAAutomationId[3:]} table")
nextHandler()

def event_gainFocus(self, obj: NVDAObject, nextHandler):
if obj.role == Role.MENUITEM and obj.UIAAutomationId == 'menuClose':
self.isFocusedOnClose = True
else:
self.isFocusedOnClose = False

if obj.role == Role.TOGGLEBUTTON and obj.UIAAutomationId == 'togEnableTun':
self.isFocusedOnEnableTon = True
else:
self.isFocusedOnEnableTon = False

if obj.role == Role.WINDOW and obj.name.startswith("v2rayN"):
if obj.role and obj.role == Role.WINDOW and obj.name and obj.name.startswith("v2rayN"):
hwnd = api.getForegroundObject().windowHandle
self.maximize_window(hwnd)

if obj.role == Role.DIALOG and obj.children:
if obj.children[1].role == Role.TOOLBAR and obj.children[2].role == Role.DATAGRID:
if obj.role and obj.role == Role.DIALOG and obj.children:
if len(obj.children) >= 3 and obj.children[1].role == Role.TOOLBAR and obj.children[2].role == Role.DATAGRID:
if len(obj.children[2].children) == 1:
obj.children[1].children[1].setFocus()
else:
obj.children[2].children[1].children[0].setFocus()
static_texts = [
obj.name for obj in obj.children if obj.role == Role.STATICTEXT]
obj.name for obj in obj.children if obj.role and obj.role == Role.STATICTEXT]
if static_texts:
for text in static_texts:
message(text)
tab_control = next(
(obj for obj in obj.children if obj.role == Role.TABCONTROL), None)
(obj for obj in obj.children if obj.role and obj.role == Role.TABCONTROL), None)
if tab_control:
tab_control.firstChild.setFocus()

nextHandler()

@ script(gesture="kb:tab")
def script_handleTabKey(self, gesture):
if self.isFocusedOnClose:
obj = api.getFocusObject()
obj = api.getFocusObject()
if obj.role and obj.role == Role.MENUITEM and obj.UIAAutomationId and obj.UIAAutomationId == 'menuClose':
try:
targetChild = obj.parent.parent.next.children[5]
if targetChild:
targetChild.setFocus()
except Exception as e:
pass
elif obj.role and obj.role == Role.TAB and obj.parent and obj.parent.parent and obj.parent.parent.role and obj.parent.parent.role == Role.DIALOG:
firstFocusableChild = next(
(obj for obj in obj.children if State.FOCUSABLE in obj.states), None)
if firstFocusableChild:
if firstFocusableChild.role and firstFocusableChild.role == Role.PANE:
firstFocusableChild = next(
(obj for obj in firstFocusableChild.children if State.FOCUSABLE in obj.states), None)
if firstFocusableChild:
firstFocusableChild.setFocus()
elif firstFocusableChild.role and firstFocusableChild.role == Role.DATAGRID:
firstFocusableChild = next(
(obj for obj in firstFocusableChild.children[1].children if State.FOCUSABLE in obj.states), None)
if firstFocusableChild:
firstFocusableChild.setFocus()
else:
firstFocusableChild.setFocus()
elif (obj.parent and obj.parent.role and obj.parent.role == Role.TAB) or (obj.parent and obj.parent.parent and obj.parent.parent.role and obj.parent.parent.role == Role.TAB) or (obj.parent and obj.parent.parent and obj.parent.parent.parent and obj.parent.parent.parent.role and obj.parent.parent.parent.role == Role.TAB):
obj = obj.parent if obj.parent.role == Role.COMBOBOX or obj.parent.role == Role.LIST else obj
lastFocusableObject = next(
(o for o in obj.parent.children[::-1] if State.FOCUSABLE in o.states), None)
if lastFocusableObject and lastFocusableObject == obj:
obj = obj.parent if obj.parent.role == Role.TAB else obj.parent.parent if obj.parent.parent.role == Role.TAB else obj.parent.parent.parent if obj.parent.parent.parent.role == Role.TAB else obj
obj = next((obj for obj in obj.parent.parent.children if obj.role and obj.role ==
Role.BUTTON and obj.UIAAutomationId and obj.UIAAutomationId == 'btnSave'), None)
if obj:
obj.setFocus()
else:
gesture.send()
elif obj.role and obj.role == Role.BUTTON and obj.UIAAutomationId and obj.UIAAutomationId == 'btnCancel' and obj.parent and obj.parent.role == Role.DIALOG:
if obj.previous and obj.previous.previous and obj.previous.previous.previous and obj.previous.previous.previous.role and obj.previous.previous.previous.role == Role.TOOLBAR:
obj.previous.previous.previous.children[1].firstChild.setFocus(
)
else:
tab_control = next(
(obj for obj in obj.parent.children if obj.role == Role.TABCONTROL), None)
if tab_control:
tab = next(
(obj for obj in tab_control.children if len(obj.children) > 1), None)
if tab:
tab.setFocus()
elif obj.parent and obj.parent.role and obj.parent.role == Role.TOOLBAR and obj.parent.parent and obj.parent.parent.role and obj.parent.parent.role == Role.DIALOG:
focusableObject = next(
(o for o in obj.parent.children[::-1] if State.FOCUSABLE in o.states), None)
if focusableObject and focusableObject == obj:
if obj.parent.role == Role.TOOLBAR:
tab_control = next(
(obj for obj in obj.parent.parent.children if obj.role and obj.role == Role.TABCONTROL), None)
if tab_control:
tab = next(
(obj for obj in tab_control.children if len(obj.children) > 1), None)
if tab:
tab.setFocus()
else:
gesture.send()
else:
gesture.send()

@ script(gesture="kb:shift+tab")
def script_handleShiftAndTabKey(self, gesture):
if self.isFocusedOnEnableTon:
obj = api.getFocusObject()
obj = api.getFocusObject()
if obj.role and obj.role == Role.TOGGLEBUTTON and obj.UIAAutomationId and obj.UIAAutomationId == 'togEnableTun':
try:
targetChild = obj.parent.previous.children[-2].firstChild
if targetChild:
targetChild.setFocus()
except Exception as e:
pass
elif obj.role and obj.role == Role.TAB and obj.parent and obj.parent.parent and obj.parent.parent.role and obj.parent.parent.role == Role.DIALOG:
obj = next(
(obj for obj in obj.parent.parent.children[::-1] if obj.role == Role.BUTTON), None)
if obj:
if obj.previous and obj.previous.previous and obj.previous.previous.previous and obj.previous.previous.previous.role and obj.previous.previous.previous.role == Role.TOOLBAR:
obj.previous.previous.previous.lastChild.setFocus()
else:
obj.setFocus()
elif (obj.parent and obj.parent.role and obj.parent.role == Role.TAB) or (obj.parent and obj.parent.parent and obj.parent.parent.role and obj.parent.parent.role == Role.TAB) or (obj.parent and obj.parent.parent and obj.parent.parent.parent and obj.parent.parent.parent.role and obj.parent.parent.parent.role == Role.TAB):
obj = obj.parent if obj.parent.role == Role.COMBOBOX or obj.parent.role == Role.LIST else obj
firstFocusableObject = next(
(o for o in obj.parent.children if State.FOCUSABLE in o.states), None)
if firstFocusableObject and firstFocusableObject == obj:
obj = obj.parent if obj.parent.role == Role.TAB else obj.parent.parent if obj.parent.parent.role == Role.TAB else obj.parent.parent.parent if obj.parent.parent.parent.role == Role.TAB else obj
obj.setFocus()
else:
gesture.send()
elif obj.role and obj.role == Role.BUTTON and obj.UIAAutomationId and obj.UIAAutomationId == 'btnSave' and obj.parent and obj.parent.role == Role.DIALOG:
tab_control = next(
(obj for obj in obj.parent.children if obj.role == Role.TABCONTROL), None)
if tab_control:
tab = next(
(obj for obj in tab_control.children if len(obj.children) > 1), None)
if tab:
lastFocusableChild = next(
(obj for obj in tab.children[::-1] if State.FOCUSABLE in obj.states), None)
if lastFocusableChild:
if lastFocusableChild.role and lastFocusableChild.role == Role.PANE:
lastFocusableChild = next(
(obj for obj in lastFocusableChild.children[::-1] if State.FOCUSABLE in obj.states), None)
if lastFocusableChild:
lastFocusableChild.setFocus()
elif lastFocusableChild.role and lastFocusableChild.role == Role.DATAGRID:
lastFocusableChild = next(
(obj for obj in lastFocusableChild.children[1].children if State.FOCUSABLE in obj.states), None)
if lastFocusableChild:
lastFocusableChild.setFocus()
else:
lastFocusableChild.setFocus()
elif obj.parent and obj.parent.parent and obj.parent.parent.role and obj.parent.parent.role == Role.TOOLBAR and obj.parent.parent.parent and obj.parent.parent.parent.role and obj.parent.parent.parent.role == Role.DIALOG:
focusableObject = next(
(o for o in obj.parent.parent.children if State.FOCUSABLE in o.states), None)
if focusableObject and focusableObject == obj.parent:
if obj.parent.parent.role == Role.TOOLBAR:
obj = next(
(obj for obj in obj.parent.parent.parent.children[::-1] if obj.role == Role.BUTTON), None)
if obj:
obj.setFocus()
else:
gesture.send()
else:
gesture.send()

@script(gesture="kb:space")
def script_handleSpace(self, gesture):
obj = api.getFocusObject()
if obj.role and obj.role == Role.MENUITEM or obj.role == Role.COMBOBOX:
keyboardHandler.KeyboardInputGesture.fromName("enter").send()
else:
gesture.send()
2 changes: 1 addition & 1 deletion buildVars.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def _(arg):
# Translators: Long description to be shown for this add-on on add-on information from add-ons manager
"addon_description": _("""Accessibility improvements for the v2rayN application"""),
# version
"addon_version": "0.5.2",
"addon_version": "0.6",
# Author(s)
"addon_author": "Mehran Latifi<mehran.latifi8383@gmail.com>",
# URL for the add-on documentation support
Expand Down

0 comments on commit 9b65818

Please sign in to comment.