Skip to content

Commit

Permalink
Refactor hierarchical bookmark logic
Browse files Browse the repository at this point in the history
  • Loading branch information
mltony committed Nov 29, 2024
1 parent a57faa1 commit 76039fd
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 63 deletions.
38 changes: 9 additions & 29 deletions addon/globalPlugins/browserNav/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,19 +631,6 @@ def browserNavPopup(selfself,gesture):
finally:
gui.mainFrame.postPopup()

def getIA2FocusedObject(obj):
if obj is None:
return None
tup = IAccessibleHandler.accFocus(obj.IAccessibleObject)
if tup is None:
return None
ia2Focus, ia2ChildId = tup
realObj = NVDAObjects.IAccessible.IAccessible(
IAccessibleObject=ia2Focus,
IAccessibleChildID=ia2ChildId,
)
return realObj

def getFocusedURL():
focus = api.getFocusObject()
if isinstance(focus, UIA):
Expand All @@ -656,23 +643,12 @@ def getFocusedURL():
# using def _get_documentConstantIdentifier from NVDAObjects/UIA/chromium.py
return obj.parent._getUIACacheablePropertyValue(UIAHandler.UIA_AutomationIdPropertyId)
# Retrieve topmost IA2 object in the window
obj = NVDAObjects.IAccessible.getNVDAObjectFromEvent(focus.windowHandle, winUser.OBJID_CLIENT, 0)
obj = utils.getIA2DocumentInThread()
if obj is None:
return None
if obj.role == controlTypes.Role.DOCUMENT:
try:
return obj.IAccessibleObject.accValue(0)
except COMError:
return None
else:
obj = getIA2FocusedObject(obj)
while obj is not None:
if obj.role == controlTypes.Role.DOCUMENT:
try:
return obj.IAccessibleObject.accValue(0)
except COMError:
return None
obj = obj.parent
try:
return obj.IAccessibleObject.accValue(0)
except COMError:
return None
elif isinstance(focus, NVDAObjects.IAccessible.IAccessible):
document = utils.getIA2Document()
Expand Down Expand Up @@ -1789,4 +1765,8 @@ def script_speakCurrentURL(self, gesture):
url = api.getCurrentURL()
#api.d = utils.getIA2Document()
#api.url = getFocusedURL()
ui.message(url)
#ui.message(url)
focus = api.getFocusObject()
textInfo = focus.treeInterceptor.makeTextInfo('caret')
x = utils.getGeckoParagraphIndent(textInfo)
ui.message(f"{x=}")
96 changes: 65 additions & 31 deletions addon/globalPlugins/browserNav/quickJump.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
import weakref
import wx
import addonHandler
from .addonConfig import getConfig

addonHandler.initTranslation()

sonifyTextInfo = None # Due to import error we set this value from __init__
Expand Down Expand Up @@ -1558,10 +1560,11 @@ def match(offset=None, message=None):
raise RuntimeError("This script is a generator function; it is only allowed for bookmark type Script and Numeric Script.")
utils.executeAsynchronously(result)
elif isinstance(result, tuple):
#match(*result)
match(*result)
elif isinstance(result, dict):
match(**result)
elif result is not None:
elif (result is not None) and (result is not False):
match(result)
except QuickJumpMatchPerformedException:
# Script called match function!
Expand Down Expand Up @@ -1844,8 +1847,44 @@ def speak():

class HierarchicalLevelsInfo:
offsets: List[int]
bracketLows: list[int]
bracketHighs: list[int]
def __init__(self, offsets):
self.offsets = offsets
self.computeBrackets()

def computeBrackets(self):
offsets = sorted(list(set(self.offsets)))
margin = getConfig('verticalAlignmentMargin')
self. bracketLows = []
self. bracketHighs = []
currentLow = currentHigh = None
for offset in offsets:
if currentLow is None:
currentLow = currentHigh = offset
elif offset <= currentHigh + margin:
currentHigh = offset
else:
self.bracketLows.append(currentLow)
self.bracketHighs.append(currentHigh)
currentLow = currentHigh = offset
if currentLow is not None:
self.bracketLows.append(currentLow)
self.bracketHighs.append(currentHigh)
n = len(self.bracketLows)
if (
(len(self.bracketLows) != len(self.bracketHighs))
or not all([self.bracketLows[i] <= self.bracketHighs[i] for i in range(n)])
or not all([self.bracketHighs[i] < self.bracketLows[i+1] for i in range(n-1)])
):
raise RuntimeError

def index(self, offset):
n = len(self.bracketLows)
for i in range(n):
if self.bracketLows[i] <= offset <= self.bracketHighs[i]:
return i
return None

hierarchicalCache = weakref.WeakKeyDictionary()
def getIndentFunc(textInfo, documentHolder, future):
Expand All @@ -1855,8 +1894,7 @@ def getIndentFunc(textInfo, documentHolder, future):
except Exception as e:
future.setException(e)

def scanLevelsThreadFunc(self, config, future, bookmarks):
#mylog("sltf begin")
def scanLevelsSync(self, config, bookmarks):
futures = []
direction = 1
try:
Expand All @@ -1880,7 +1918,6 @@ def scanLevelsThreadFunc(self, config, future, bookmarks):
innerFuture = utils.Future()
utils.threadPool.add_task(getIndentFunc, matchInfo, documentHolder, innerFuture)
futures.append(innerFuture)

distance += 1
result = moveParagraph(textInfo, direction)
if result == 0:
Expand All @@ -1889,27 +1926,14 @@ def scanLevelsThreadFunc(self, config, future, bookmarks):
inner.get()
for inner in futures
})))
future.set(result)
#mylog("sltf success")
#mylog(f"sltf result={result.offsets}")
return
return result
except Exception as e:
#mylog("sltf fail")
future.setException(e)

raise e

def scanLevels(self, bookmarks):
global globalConfig, hierarchicalCache
config = globalConfig
future = utils.Future()
utils.threadPool.add_task(scanLevelsThreadFunc, self, config, future, bookmarks)
try:
innerDict = hierarchicalCache[self]
except KeyError:
innerDict = {}
hierarchicalCache[self] = innerDict
innerDict[config] = future
return future
result = scanLevelsSync(self, globalConfig, bookmarks)
return result

def hierarchicalQuickJump(self, gesture, category, direction, level, unbounded, errorMsg):
url = getUrl(self)
Expand All @@ -1927,6 +1951,7 @@ def hierarchicalQuickJump(self, gesture, category, direction, level, unbounded,
return endOfDocument(_('No hierarchical quickJump bookmarks or numeric script bookmarks configured for current website. Please add QuickJump bookmarks in BrowserNav settings in NVDA settings window.'))

def _hierarchicalQuickJump(self, gesture, category, direction, level, unbounded, errorMsg):
global hierarchicalCache
oldSelection = self.selection
url = getUrl(self)
bookmarks = findApplicableBookmarks(globalConfig, url, category)
Expand All @@ -1935,12 +1960,16 @@ def _hierarchicalQuickJump(self, gesture, category, direction, level, unbounded,
if len(bookmarks) == 0:
return endOfDocument(_('No hierarchical quickJump bookmarks configured for current website. Please add QuickJump bookmarks in BrowserNav settings in NVDA settings window.'))
try:
levelsInfo = hierarchicalCache[self][globalConfig].get()
levelsInfo = hierarchicalCache[self][globalConfig]
except KeyError:
levelsInfo = None
scanLevels(self, bookmarks)
mylog(f"levelsInfo is None")
levelsInfo = hierarchicalCache[self][globalConfig].get()
levelsInfo = scanLevels(self, bookmarks)
try:
innerDict = hierarchicalCache[self]
except KeyError:
innerDict = {}
hierarchicalCache[self] = innerDict
innerDict[globalConfig] = levelsInfo
mylog(f"level={level} levelsInfo={levelsInfo.offsets}")
textInfo = self.makeTextInfo(textInfos.POSITION_CARET)
textInfo.collapse()
Expand Down Expand Up @@ -1969,12 +1998,14 @@ def _hierarchicalQuickJump(self, gesture, category, direction, level, unbounded,
offset = utils.getGeckoParagraphIndent(thisInfo, documentHolder)
mylog(f"thisInfo={thisInfo.text}")
mylog(f"offset={offset}")
currentLevel = levelsInfo.index(offset)
if (
levelsInfo is None
or level is None
or (
offset in levelsInfo.offsets
and levelsInfo.offsets.index(offset) == level
#offset in levelsInfo.offsets
#and levelsInfo.offsets.index(offset) == level
currentLevel == level
)
):
mylog("Perfect")
Expand All @@ -1983,7 +2014,7 @@ def _hierarchicalQuickJump(self, gesture, category, direction, level, unbounded,
and levelsInfo is not None
and offset in levelsInfo.offsets
):
announceLevel = levelsInfo.offsets.index(offset) + 1
announceLevel = levelsInfo.index(offset) + 1
ui.message(_("Level {announceLevel}").format(announceLevel=announceLevel))
if message is not None and len(message) > 0:
ui.message(message)
Expand All @@ -1994,16 +2025,19 @@ def _hierarchicalQuickJump(self, gesture, category, direction, level, unbounded,
self.selection = thisInfo
sonifyTextInfo(self.selection, oldTextInfo=oldSelection, includeCrackle=True)
return
elif offset not in levelsInfo.offsets:
#elif offset not in levelsInfo.offsets:
elif currentLevel is None:
# Something must have happened that current level is not recorded in the previous scan. Rescan after this script.
mylog("offset not in levelsInfo")
scanLevels(self, bookmarks)
endOfDocument(_("BrowserNav error: inconsistent indents in the document. Recomputing indents, please try again."))
return
elif levelsInfo.offsets.index(offset) > level:
#elif levelsInfo.offsets.index(offset) > level:
elif currentLevel > level:
#mylog("levelsInfo.offsets.index(offset) > level")
continue
elif levelsInfo.offsets.index(offset) < level:
#elif levelsInfo.offsets.index(offset) < level:
elif currentLevel < level:
#mylog("levelsInfo.offsets.index(offset) < level")
if unbounded:
continue
Expand Down
46 changes: 43 additions & 3 deletions addon/globalPlugins/browserNav/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import winUser
import api
import itertools
from logHandler import log
import NVDAObjects.IAccessible

class FakeObjectForWeakMemoize:
pass
Expand Down Expand Up @@ -167,10 +169,48 @@ def getIA2Document(textInfo=None):
return obj
return None

def getIA2FocusedObject(obj):
if obj is None:
return None
tup = IAccessibleHandler.accFocus(obj.IAccessibleObject)
if tup is None:
return None
ia2Focus, ia2ChildId = tup
realObj = NVDAObjects.IAccessible.IAccessible(
IAccessibleObject=ia2Focus,
IAccessibleChildID=ia2ChildId,
)
return realObj

def getIA2DocumentInThread():
focus = api.getFocusObject()
obj = NVDAObjects.IAccessible.getNVDAObjectFromEvent(focus.windowHandle, winUser.OBJID_CLIENT, 0)
if obj is None:
return None
if obj.role == controlTypes.Role.DOCUMENT:
return obj
else:
obj = getIA2FocusedObject(obj)
while obj is not None:
if obj.role == controlTypes.Role.DOCUMENT:
return obj
obj = obj.parent
return None

class DocumentHolder:
def __init__(self, document):
self.document = document
self.originalDocument = document
self.localDocument = threading.local()
self.localDocument.document = document

def getDocument(self):
try:
return self.localDocument.document
except AttributeError:
document = getIA2DocumentInThread()
if document is not None:
self.localDocument.document = document
return document

def getGeckoParagraphIndent(textInfo, documentHolder=None, oneLastAttempt=False):
if not isinstance(textInfo, Gecko_ia2_TextInfo):
Expand All @@ -189,7 +229,7 @@ def getGeckoParagraphIndent(textInfo, documentHolder=None, oneLastAttempt=False)
if documentHolder is None:
document = getIA2Document(textInfo)
else:
document = documentHolder.document
document = documentHolder.getDocument()
offset = textInfo._startOffset
docHandle,ID=textInfo._getFieldIdentifierFromOffset(offset)
location = document.IAccessibleObject.accLocation(ID)
Expand All @@ -198,7 +238,7 @@ def getGeckoParagraphIndent(textInfo, documentHolder=None, oneLastAttempt=False)
return None
except LookupError:
return None
except _ctypes.COMError:
except _ctypes.COMError as e:
if oneLastAttempt or documentHolder is None:
return None
# This tends to happen when page changes dynamically.
Expand Down

0 comments on commit 76039fd

Please sign in to comment.