-
-
Notifications
You must be signed in to change notification settings - Fork 801
Add visual highlighting for web math navigation #20372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 20 commits
10cfd8f
a55dc3a
a0cd4f3
302b494
1b70876
946697a
c971fe1
ce5ed03
26b22cf
83a159a
043b31f
9c51e9c
fc92759
7a6c02e
86b691f
9f94dad
2f8d2b4
f4a1617
2401f41
ae267e9
4871123
25d1d66
78684a9
b58914f
5fcd252
2f24dd3
88022c4
3a97a79
5ba601d
61f5629
2bc0545
2543636
f31bc0d
19336ad
71bacfe
d11a100
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,7 +12,7 @@ | |
| windll, | ||
| ) | ||
| from os import path | ||
| from typing import Type | ||
| from typing import TYPE_CHECKING, Type | ||
|
|
||
| import braille | ||
| import config | ||
|
|
@@ -43,9 +43,18 @@ | |
| import mathPres | ||
| from .localization import getLanguageToUse | ||
| from .navCommands import NAV_COMMANDS | ||
| from .navNodeMapping import ( | ||
| NAV_NODE_ID_PREFIX, | ||
| prepareMathMlForNavigation, | ||
| removeSyntheticIdsFromMathMl, | ||
| ) | ||
| from .preferences import applyUserPreferences | ||
| from .speech import convertSSMLTextForNVDA | ||
|
|
||
| if TYPE_CHECKING: | ||
| from locationHelper import RectLTRB | ||
| from NVDAObjects import NVDAObject | ||
|
|
||
| # Translators: The name of the category of MathCAT navigation commands in the Input Gestures dialog. | ||
| SCRCAT_MATHCAT_NAV = _("MathCat navigation") | ||
|
|
||
|
|
@@ -68,13 +77,19 @@ def __init__( | |
| self, | ||
| provider: mathPres.MathPresentationProvider | None = None, | ||
| mathMl: str | None = None, | ||
| sourceObj: "NVDAObject | None" = None, | ||
| ): | ||
| """Initialize the MathCATInteraction object. | ||
|
|
||
| :param provider: Optional presentation provider. | ||
| :param mathMl: Optional initial MathML string. | ||
| :param sourceObj: Optional source object containing the math. | ||
| """ | ||
| super(MathCATInteraction, self).__init__(provider=provider, mathMl=mathMl) | ||
| super(MathCATInteraction, self).__init__(provider=provider, mathMl=mathMl, sourceObj=sourceObj) | ||
| self.mathMlForNavigation, self._mathNodeRectsById = prepareMathMlForNavigation( | ||
| mathMl or "<math></math>", | ||
| sourceObj, | ||
| ) | ||
| if mathMl is None: | ||
| self.initMathML = "<math></math>" | ||
| else: | ||
|
|
@@ -86,10 +101,65 @@ def reportFocus(self) -> None: | |
| try: | ||
| text: str = libmathcat.DoNavigateCommand("ZoomIn") | ||
| speech.speak(convertSSMLTextForNVDA(text)) | ||
| self._updateMathHighlight() | ||
| except Exception: | ||
| log.exception() | ||
| # Translators: this message reports an error in starting navigation of math. | ||
| ui.message(pgettext("math", "Error in starting navigation of math.")) | ||
| self._clearMathHighlight() | ||
|
|
||
| def _getHighlightRect(self) -> "RectLTRB | None": | ||
| """Get the navigation rectangle for a supported web math source object.""" | ||
| sourceObj = self.sourceObj | ||
| if not sourceObj: | ||
| return None | ||
| from NVDAObjects.IAccessible.ia2Web import Math as Ia2WebMath | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can these imports be moved out?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The other imports should definitely be moved out, but it seems to me like it would be a good idea to keep this import local. I've added a comment with my reasoning for that. I'm still happy to move it out if you think that's best though. |
||
|
|
||
| if not isinstance(sourceObj, Ia2WebMath): | ||
| return None | ||
| try: | ||
| nodeId = libmathcat.GetNavigationMathMLId()[0] | ||
| except Exception: | ||
| log.debugWarning("Error getting MathCAT navigation node id", exc_info=True) | ||
| else: | ||
| if nodeId in self._mathNodeRectsById: | ||
| return self._mathNodeRectsById[nodeId] | ||
| if nodeId.startswith(NAV_NODE_ID_PREFIX): | ||
| log.debug( | ||
| f"Math highlight found synthetic MathML id {nodeId!r}, but it has no mapped IA2 rectangle", | ||
| ) | ||
| log.debug(f"Math highlight falling back to source rectangle for node id {nodeId!r}") | ||
| try: | ||
| if sourceObj.hasIrrelevantLocation: | ||
| return None | ||
| location = sourceObj.location | ||
|
RyanMcCleary marked this conversation as resolved.
Outdated
|
||
| except Exception: | ||
| log.debugWarning("Error getting math source location", exc_info=True) | ||
| return None | ||
| return location.toLTRB() if location else None | ||
|
|
||
| def _updateMathHighlight(self) -> None: | ||
| try: | ||
| rect = self._getHighlightRect() | ||
| except Exception: | ||
| log.debugWarning("Error updating math highlight", exc_info=True) | ||
| rect = None | ||
| try: | ||
| import vision | ||
|
RyanMcCleary marked this conversation as resolved.
Outdated
|
||
|
|
||
| if vision.handler: | ||
| vision.handler.handleMathNavigation(rect) | ||
| except Exception: | ||
| log.debugWarning("Error sending math highlight update", exc_info=True) | ||
|
|
||
| def _clearMathHighlight(self) -> None: | ||
| try: | ||
| import vision | ||
|
RyanMcCleary marked this conversation as resolved.
Outdated
|
||
|
|
||
| if vision.handler: | ||
| vision.handler.handleMathNavigation(None) | ||
| except Exception: | ||
|
RyanMcCleary marked this conversation as resolved.
Outdated
|
||
| log.debugWarning("Error clearing math highlight", exc_info=True) | ||
|
|
||
| def getBrailleRegions( | ||
| self, | ||
|
|
@@ -117,10 +187,12 @@ def _doNavigateCommand(self, commandName: str) -> None: | |
| try: | ||
| text = libmathcat.DoNavigateCommand(commandName) | ||
| speech.speak(convertSSMLTextForNVDA(text)) | ||
| self._updateMathHighlight() | ||
| except Exception: | ||
| log.exception() | ||
| # Translators: this message alerts users to an error in navigating math. | ||
| ui.message(pgettext("math", "Error in navigating math")) | ||
| self._clearMathHighlight() | ||
|
|
||
| self._updateBraille() | ||
|
|
||
|
|
@@ -197,7 +269,7 @@ def script_rawdataToClip(self, gesture: KeyboardInputGesture) -> None: | |
| mathml = self.initMathML | ||
| if copyAs == "speech": | ||
| # save the old MathML, set the navigation MathML as MathMl, get the speech, then reset the MathML | ||
| savedMathML: str = self.initMathML | ||
| savedMathML: str = self.mathMlForNavigation | ||
| savedTTS: str = libmathcat.GetPreference("TTS") | ||
| if savedMathML == "": # shouldn't happen | ||
| raise Exception("Internal error -- MathML not set for copy") | ||
|
|
@@ -220,12 +292,11 @@ def script_rawdataToClip(self, gesture: KeyboardInputGesture) -> None: | |
|
|
||
| # not a perfect match sequence, but should capture normal MathML | ||
| _mathTagHasNameSpace: re.Pattern = re.compile("<math .*?xmlns.+?>") | ||
| _hasAddedId: re.Pattern = re.compile(" id='[^'].+' data-id-added='true'") | ||
| _hasDataAttr: re.Pattern = re.compile(" data-[^=]+='[^']*'") | ||
| _hasDataAttr: re.Pattern = re.compile(r""" data-[^=]+=(['"]).*?\1""") | ||
|
|
||
| def _wrapMathMLForClipBoard(self, text: str) -> str: | ||
| """Cleanup the MathML a little.""" | ||
| text = re.sub(self._hasAddedId, "", text) | ||
| text = removeSyntheticIdsFromMathMl(text) | ||
| mathMLWithNS: str = re.sub(self._hasDataAttr, "", text) | ||
| if not re.match(self._mathTagHasNameSpace, mathMLWithNS): | ||
| mathMLWithNS = mathMLWithNS.replace( | ||
|
|
@@ -297,6 +368,8 @@ def _setClipboardData(self, format: int, data: str) -> None: | |
|
|
||
|
|
||
| class MathCAT(mathPres.MathPresentationProvider): | ||
| supportsInteractionSourceObj: bool = True | ||
|
|
||
| def __init__(self): | ||
| """Initializes MathCAT, loading the rules specified in the rules directory.""" | ||
|
|
||
|
|
@@ -398,11 +471,20 @@ def getBrailleForMathMl(self, mathml: str) -> str: | |
| ui.message(pgettext("math", "Error in brailling math.")) | ||
| return "" | ||
|
|
||
| def interactWithMathMl(self, mathml: str) -> None: | ||
| def interactWithMathMl(self, mathml: str, sourceObj: "NVDAObject | None" = None) -> None: | ||
| """Interact with a MathML string, creating a MathCATInteraction object. | ||
|
|
||
| :param mathml: The MathML representing the math to interact with. | ||
| :param sourceObj: Optional source object containing the math. | ||
| """ | ||
| interaction = MathCATInteraction(provider=self, mathMl=mathml) | ||
| interaction = MathCATInteraction(provider=self, mathMl=mathml, sourceObj=sourceObj) | ||
| try: | ||
| libmathcat.SetMathML(interaction.mathMlForNavigation) | ||
| except Exception: | ||
| log.exception(f"MathML is {interaction.mathMlForNavigation}") | ||
| # Translators: this message reports illegal MathML. | ||
| ui.message(pgettext("math", "Invalid MathML found.")) | ||
| libmathcat.SetMathML("<math></math>") | ||
| return | ||
| interaction.setFocus() | ||
| interaction._updateBraille() | ||
Uh oh!
There was an error while loading. Please reload this page.