Skip to content

Commit

Permalink
Merge branch 'master' into brailleWordWrap
Browse files Browse the repository at this point in the history
  • Loading branch information
michaelDCurran committed Aug 29, 2024
2 parents 1bccb9b + 37b4046 commit 12d916a
Show file tree
Hide file tree
Showing 21 changed files with 452 additions and 8,357 deletions.
3 changes: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
*.mo
source/comInterfaces/*.py
!source/comInterfaces/__init__.py
# These are manually generated and updated, so don't delete them
!source/comInterfaces/UIAutomationClient.py
!source/comInterfaces/_944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.py
*.log
source/userConfig
build
Expand Down
4 changes: 0 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,12 @@ repos:
exclude: "\\.(dic|sfd)$"
# Checks python syntax
- id: check-ast
# File uses encoding not supported in Linux
exclude: source/comInterfaces/_944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.py
# Checks for filenames that will conflict on case insensitive filesystems (the majority of Windows filesystems, most of the time)
- id: check-case-conflict
# Checks for artifacts from resolving merge conflicts.
- id: check-merge-conflict
# Checks Python files for debug statements, such as python's breakpoint function, or those inserted by some IDEs.
- id: debug-statements
# File uses encoding not supported in Linux
exclude: source/comInterfaces/_944DE083_8FB8_45CF_BCB7_C477ACB2F897_0_1_0.py
# Removes trailing whitespace.
- id: trailing-whitespace
types_or: [python, c, c++, batch, markdown]
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
SCons==4.5.2

# NVDA's runtime dependencies
comtypes==1.2.0
comtypes==1.4.6
pyserial==3.5
wxPython @ https://github.com/nvaccess/nvda-misc-deps/raw/51ae7db821d1d5166ab0c030fe20ec72dd7a2ad9/python/wxPython-4.2.2a1-cp311-cp311-win32.whl
configobj @ git+https://github.com/DiffSK/configobj@e2ba4457c4651fa54f8d59d8dcdd3da950e956b8#egg=configobj
Expand Down
21 changes: 21 additions & 0 deletions source/NVDAObjects/IAccessible/chromium.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,25 @@ def _get_role(self) -> controlTypes.Role:
return controlTypes.Role.FIGURE


class EditorTextInfo(ia2Web.MozillaCompoundTextInfo):
"""The TextInfo for edit areas such as edit fields and documents in Chromium."""

def _isCaretAtEndOfLine(self, caretObj: IAccessible) -> bool:
# Detecting if the caret is at the end of the line in Chromium is not currently possible
# as Chromium's IAccessibleText::textAtOffset with IA2_OFFSET_CARET returns the first character of the next line,
# which is what we are trying to avoid in the first place.
# #17039: In some scenarios such as in large files in VS Code,
# this call is extreamly costly for no actual bennifit right now,
# so we are disabling it for Chromium.
return False


class Editor(ia2Web.Editor):
"""The NVDAObject for edit areas such as edit fields and documents in Chromium."""

TextInfo = EditorTextInfo


def findExtraOverlayClasses(obj, clsList):
"""Determine the most appropriate class(es) for Chromium objects.
This works similarly to L{NVDAObjects.NVDAObject.findOverlayClasses} except that it never calls any other findOverlayClasses method.
Expand All @@ -188,3 +207,5 @@ def findExtraOverlayClasses(obj, clsList):
clsList,
documentClass=Document,
)
if ia2Web.Editor in clsList:
clsList.insert(0, Editor)
18 changes: 0 additions & 18 deletions source/appModules/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,23 @@

import appModuleHandler
import controlTypes
from NVDAObjects.IAccessible.ia2Web import Editor
from NVDAObjects.IAccessible.chromium import Document
from NVDAObjects.IAccessible.ia2TextMozilla import MozillaCompoundTextInfo
from NVDAObjects import NVDAObject, NVDAObjectTextInfo


class VSCodeEditorTextInfo(MozillaCompoundTextInfo):
def _isCaretAtEndOfLine(self, caretObj) -> bool:
# #17039: IAccessibleText::textAtOffset with IA2_OFFSET_CARET is way too costly in VSCode.
# And doesn't actually work correctly in Chromium yet anyway.
# So it is best not to try detecting for now.
return False


class VSCodeEditor(Editor):
TextInfo = VSCodeEditorTextInfo


class VSCodeDocument(Document):
"""The only content in the root document node of Visual Studio code is the application object.
Creating a tree interceptor on this object causes a major slow down of Code.
Therefore, forcefully block tree interceptor creation.
"""

textInfo = VSCodeEditorTextInfo

_get_treeInterceptorClass = NVDAObject._get_treeInterceptorClass


class AppModule(appModuleHandler.AppModule):
def chooseNVDAObjectOverlayClasses(self, obj, clsList):
if Document in clsList and obj.IA2Attributes.get("tag") == "#document":
clsList.insert(0, VSCodeDocument)
elif Editor in clsList:
clsList.insert(0, VSCodeEditor)

def event_NVDAObject_init(self, obj: NVDAObject):
# This is a specific fix for Visual Studio Code,
Expand Down
173 changes: 118 additions & 55 deletions source/appModules/powerpnt.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2012-2022 NV Access Limited
# Copyright (C) 2012-2024 NV Access Limited, Leonard de Ruijter
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

from typing import (
Any,
Optional,
Dict,
)
Expand All @@ -12,6 +13,8 @@
from comtypes.automation import IDispatch
import comtypes.client
import ctypes

import comtypes.client.lazybind
import oleacc
import comHelper
import ui
Expand Down Expand Up @@ -1043,44 +1046,128 @@ def script_enterChart(self, gesture):


class TextFrameTextInfo(textInfos.offsets.OffsetsTextInfo):
def _getCaretOffset(self):
"""
TextInfo for a PowerPoint text frame,
fetching its information from a TextRange in the PowerPoint object model.
For more information about text ranges, see https://learn.microsoft.com/en-us/office/vba/api/powerpoint.textrange
"""

def _getCaretOffset(self) -> int:
return self.obj.documentWindow.ppSelection.textRange.start - 1

def _getSelectionOffsets(self):
def _getSelectionOffsets(self) -> tuple[int, int]:
sel = self.obj.documentWindow.ppSelection.textRange
start = sel.start - 1
end = start + sel.length
return start, end

def _getTextRange(self, start, end):
# #4619: First let's "normalise" the text, i.e. get rid of the CR/LF mess
text = self.obj.ppObject.textRange.text
text = text.replace("\r\n", "\n")
# Now string slicing will be okay
text = text[start:end].replace("\x0b", "\n")
text = text.replace("\r", "\n")
return text
def _getPptTextRange(
self,
start: int,
end: int,
clamp: bool = False,
) -> comtypes.client.lazybind.Dispatch:
"""
Retrieves a text range from the PowerPoint object, with optional clamping of the start and end indices.
:param start: The starting character index of the text range (zero based).
:param end: The ending character index of the text range(zero based).
:param clamp: If True, the start and end indices will be clamped to valid values within the text range. Defaults to False.
:returns: The text range object as a comtypes.client.lazybind.Dispatch object.
:raises ValueError: If the start index is greater than the end index.
"""
if not (start <= end):
raise ValueError(
f"start must be less than or equal to end. Got {start=}, {end=}.",
stack_info=True,
)
maxLength = self._getStoryLength()
# Having start = maxLength does not make sense, as there will be no selection if this is the case.
if not (0 <= start < maxLength) and clamp:
log.debugWarning(
f"Got out of range {start=} (min 0, max {maxLength - 1}. Clamping.",
stack_info=True,
)
start = max(0, min(start, maxLength - 1))
# Having end = 0 does not make sense, as there will be no selection if this is the case.
if not (0 < end <= maxLength) and clamp:
log.debugWarning(f"Got out of range {end=} (min 1, max {maxLength}. Clamping.", stack_info=True)
end = max(1, min(end, maxLength))
# The TextRange.characters method is 1-indexed.
return self.obj.ppObject.textRange.characters(start + 1, end - start)

def _getTextRange(self, start: int, end: int) -> str:
"""
Retrieves the text content of a PowerPoint text range, replacing any newline characters with standard newline characters.
:param start: The starting character index of the text range (zero based).
:param end: The ending character index (zero based) of the text range.
:returns: The text content of the specified text range, with newline characters normalized.
"""
return self._getPptTextRange(start, end).text.replace("\x0b", "\n")

def _getStoryLength(self):
def _getStoryLength(self) -> int:
return self.obj.ppObject.textRange.length

def _getLineOffsets(self, offset):
# Seems to be no direct way to find the line offsets for a given offset.
# Therefore walk through all the lines until one surrounds the offset.
lines = self.obj.ppObject.textRange.lines()
length = lines.length
# #3403: handle case where offset is at end of the text in in a control with only one line
# The offset should be limited to the last offset in the text, but only if the text does not end in a line feed.
if length and offset >= length and self._getTextRange(length - 1, length) != "\n":
offset = min(offset, length - 1)
for line in lines:
start = line.start - 1
end = start + line.length
if start <= offset < end:
return start, end
def _getCharacterOffsets(self, offset: int) -> tuple[int, int]:
range = self.obj.ppObject.textRange.characters(offset + 1, 1)
start = range.start - 1
end = start + range.length
if start <= offset < end:
return start, end
return offset, offset + 1

def _getFormatFieldAndOffsets(self, offset, formatConfig, calculateOffsets=True):
@staticmethod
def _getOffsets(ranges: comtypes.client.lazybind.Dispatch, offset: int) -> tuple[int, int, int]:
"""
Retrieves the start and end offsets of a range of elements
(e.g. words, lines, paragraphs) within a text range, given a specific offset.
:param ranges: The collection of elements (e.g. words, lines, paragraphs) to search.
These are retrieved by calling a method on the text range object, such as textRange.words(), textRange.lines(), or textRange.paragraphs().
:param offset: The zero based character offset to search for within the text range.
:Returns: A tuple containing the index of the element that contains the given offset, the zero based start offset of that element, and the zero based end offset of that element.
"""
for i, chunk in enumerate(ranges):
start = chunk.start - 1
end = start + chunk.length
if start <= offset < end:
return i, start, end
return 0, offset, offset + 1

def _getWordOffsets(self, offset) -> tuple[int, int]:
return self._getOffsets(self.obj.ppObject.textRange.words(), offset)[1:]

def _getLineNumFromOffset(self, offset: int) -> int:
return self._getOffsets(self.obj.ppObject.textRange.lines(), offset)[0]

def _getLineOffsets(self, offset: int) -> tuple[int, int]:
return self._getOffsets(self.obj.ppObject.textRange.lines(), offset)[1:]

def _getParagraphOffsets(self, offset: int) -> tuple[int, int]:
return self._getOffsets(self.obj.ppObject.textRange.paragraphs(), offset)[1:]

def _getSentenceOffsets(self, offset: int) -> tuple[int, int]:
return self._getOffsets(self.obj.ppObject.textRange.sentences(), offset)[1:]

def _getBoundingRectFromOffset(self, offset: int) -> RectLTRB:
range = self.obj.ppObject.textRange.characters(offset + 1, 1)
try:
rangeLeft = range.BoundLeft
rangeTop = range.boundTop
rangeWidth = range.BoundWidth
rangeHeight = range.BoundHeight
except comtypes.COMError as e:
raise LookupError from e
left = self.obj.documentWindow.ppObjectModel.pointsToScreenPixelsX(rangeLeft)
top = self.obj.documentWindow.ppObjectModel.pointsToScreenPixelsY(rangeTop)
right = self.obj.documentWindow.ppObjectModel.pointsToScreenPixelsX(rangeLeft + rangeWidth)
bottom = self.obj.documentWindow.ppObjectModel.pointsToScreenPixelsY(rangeTop + rangeHeight)
return RectLTRB(left, top, right, bottom)

def _getFormatFieldAndOffsets(
self,
offset: int,
formatConfig: dict[str, Any],
calculateOffsets: bool = True,
) -> tuple[textInfos.FormatField, tuple[int, int]]:
formatField = textInfos.FormatField()
curRun = None
if calculateOffsets:
Expand Down Expand Up @@ -1128,35 +1215,11 @@ def _getFormatFieldAndOffsets(self, offset, formatConfig, calculateOffsets=True)
formatField["link"] = True
return formatField, (startOffset, endOffset)

def _setCaretOffset(self, offset: int):
if not (0 <= offset <= (maxLength := self._getStoryLength())):
log.debugWarning(
f"Got out of range {offset=} (min 0, max {maxLength}. Clamping.",
stack_info=True,
)
offset = max(0, min(offset, maxLength))
# Use the TextRange.select method to move the text caret to a 0-length TextRange.
# The TextRange.characters method is 1-indexed.
self.obj.ppObject.textRange.characters(offset + 1, 0).select()
def _setCaretOffset(self, offset: int) -> None:
return self._setSelectionOffsets(offset, offset)

def _setSelectionOffsets(self, start: int, end: int):
if not start < end:
log.debug(f"start must be less than end. Got {start=}, {end=}.", stack_info=True)
return
maxLength = self._getStoryLength()
# Having start = maxLength does not make sense, as there will be no selection if this is the case.
if not (0 <= start < maxLength):
log.debugWarning(
f"Got out of range {start=} (min 0, max {maxLength - 1}. Clamping.",
stack_info=True,
)
start = max(0, min(start, maxLength - 1))
# Having end = 0 does not make sense, as there will be no selection if this is the case.
if not (0 < end <= maxLength):
log.debugWarning(f"Got out of range {end=} (min 1, max {maxLength}. Clamping.", stack_info=True)
end = max(1, min(end, maxLength))
# The TextRange.characters method is 1-indexed.
self.obj.ppObject.textRange.characters(start + 1, end - start).select()
def _setSelectionOffsets(self, start: int, end: int) -> None:
self._getPptTextRange(start, end, clamp=True).select()


class Table(Shape):
Expand Down
7 changes: 0 additions & 7 deletions source/comInterfaces/UIAutomationClient.py

This file was deleted.

Loading

0 comments on commit 12d916a

Please sign in to comment.