Skip to content

Commit

Permalink
Merge cea6aca into b21a299
Browse files Browse the repository at this point in the history
  • Loading branch information
codeofdusk authored Jan 24, 2020
2 parents b21a299 + cea6aca commit 1faa8e9
Show file tree
Hide file tree
Showing 7 changed files with 232 additions and 153 deletions.
3 changes: 2 additions & 1 deletion source/NVDAObjects/UIA/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import textInfos
from logHandler import log
from UIAUtils import *
from UIAUtils import shouldUseUIAConsole
from NVDAObjects.window import Window
from NVDAObjects import NVDAObjectTextInfo, InvalidNVDAObject
from NVDAObjects.behaviors import (
Expand Down Expand Up @@ -929,7 +930,7 @@ def findOverlayClasses(self,clsList):
# Support Windows Console's UIA interface
if (
self.windowClassName == "ConsoleWindowClass"
and config.conf['UIA']['winConsoleImplementation'] == "UIA"
and shouldUseUIAConsole()
):
from . import winConsoleUIA
winConsoleUIA.findExtraOverlayClasses(self, clsList)
Expand Down
234 changes: 122 additions & 112 deletions source/NVDAObjects/UIA/winConsoleUIA.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# A part of NonVisual Desktop Access (NVDA)
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.
# Copyright (C) 2019 Bill Dengler
# Copyright (C) 2019-2020 Bill Dengler

import ctypes
import NVDAHelper
Expand All @@ -12,13 +12,13 @@

from comtypes import COMError
from UIAUtils import isTextRangeOffscreen
from winVersion import isWin10
from . import UIATextInfo
from ..behaviors import KeyboardHandlerBasedTypedCharSupport
from ..window import Window


class consoleUIATextInfo(UIATextInfo):

def __init__(self, obj, position, _rangeObj=None):
# We want to limit textInfos to just the visible part of the console.
# Therefore we specifically handle POSITION_FIRST, POSITION_LAST and POSITION_ALL.
Expand Down Expand Up @@ -48,8 +48,58 @@ def __init__(self, obj, position, _rangeObj=None):
_rangeObj = first._rangeObj
super(consoleUIATextInfo, self).__init__(obj, position, _rangeObj)

def move(self, unit, direction, endPoint=None):
oldInfo = None
if self.basePosition != textInfos.POSITION_CARET:
# Insure we haven't gone beyond the visible text.
# UIA adds thousands of blank lines to the end of the console.
boundingInfo = self.obj.makeTextInfo(textInfos.POSITION_ALL)
oldInfo = self.copy()
res = self._move(unit, direction, endPoint)
# Console textRanges have access to the entire console buffer.
# However, we want to limit ourselves to onscreen text.
# Therefore, if the textInfo was originally visible,
# but we are now above or below the visible range,
# Restore the original textRange and pretend the move didn't work.
if oldInfo:
try:
if (
(
self.compareEndPoints(boundingInfo, "startToStart") < 0
or self.compareEndPoints(boundingInfo, "startToEnd") >= 0
)
and not (
oldInfo.compareEndPoints(boundingInfo, "startToStart") < 0
or oldInfo.compareEndPoints(boundingInfo, "startToEnd") >= 0
)
):
self._rangeObj = oldInfo._rangeObj
return 0
except (COMError, RuntimeError):
pass
return res

def _move(self, unit, direction, endPoint=None):
"Perform a move without respect to bounding."
return super(consoleUIATextInfo, self).move(unit, direction, endPoint)

def __ne__(self, other):
"""Support more accurate caret move detection."""
return not self == other

def _get_text(self):
# #10036: return a space if the text range is empty.
# Consoles don't actually store spaces, the character is merely left blank.
res = super(consoleUIATextInfo, self)._get_text()
if not res:
return ' '
else:
return res


class consoleUIATextInfoPre2004(consoleUIATextInfo):
def collapse(self, end=False):
"""Works around a UIA bug on Windows 10 1803 and later."""
"""Works around a UIA bug on Windows 10 1803 to 2004."""
# When collapsing, consoles seem to incorrectly push the start of the
# textRange back one character.
# Correct this by bringing the start back up to where the end is.
Expand All @@ -62,13 +112,62 @@ def collapse(self, end=False):
UIAHandler.TextPatternRangeEndpoint_Start
)

def move(self, unit, direction, endPoint=None):
oldInfo = None
if self.basePosition != textInfos.POSITION_CARET:
# Insure we haven't gone beyond the visible text.
# UIA adds thousands of blank lines to the end of the console.
boundingInfo = self.obj.makeTextInfo(textInfos.POSITION_ALL)
oldInfo = self.copy()
def compareEndPoints(self, other, which):
"""Works around a UIA bug on Windows 10 1803 to 2004."""
# Even when a console textRange's start and end have been moved to the
# same position, the console incorrectly reports the end as being
# past the start.
# Compare to the start (not the end) when collapsed.
selfEndPoint, otherEndPoint = which.split("To")
if selfEndPoint == "end" and self._isCollapsed():
selfEndPoint = "start"
if otherEndPoint == "End" and other._isCollapsed():
otherEndPoint = "Start"
which = f"{selfEndPoint}To{otherEndPoint}"
return super().compareEndPoints(other, which=which)

def setEndPoint(self, other, which):
"""Override of L{textInfos.TextInfo.setEndPoint}.
Works around a UIA bug on Windows 10 1803 to 2004 that means we can
not trust the "end" endpoint of a collapsed (empty) text range
for comparisons.
"""
selfEndPoint, otherEndPoint = which.split("To")
# In this case, there is no need to check if self is collapsed
# since the point of this method is to change its text range, modifying the "end" endpoint of a collapsed
# text range is fine.
if otherEndPoint == "End" and other._isCollapsed():
otherEndPoint = "Start"
which = f"{selfEndPoint}To{otherEndPoint}"
return super().setEndPoint(other, which=which)

def expand(self, unit):
if unit == textInfos.UNIT_WORD:
# UIA doesn't implement word movement, so we need to do it manually.
lineInfo = self.copy()
lineInfo.expand(textInfos.UNIT_LINE)
offset = self._getCurrentOffsetInThisLine(lineInfo)
start, end = self._getWordOffsetsInThisLine(offset, lineInfo)
wordEndPoints = (
(offset - start) * -1,
end - offset - 1
)
if wordEndPoints[0]:
self._rangeObj.MoveEndpointByUnit(
UIAHandler.TextPatternRangeEndpoint_Start,
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
wordEndPoints[0]
)
if wordEndPoints[1]:
self._rangeObj.MoveEndpointByUnit(
UIAHandler.TextPatternRangeEndpoint_End,
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
wordEndPoints[1]
)
else:
return super(consoleUIATextInfo, self).expand(unit)

def _move(self, unit, direction, endPoint=None):
if unit == textInfos.UNIT_WORD and direction != 0:
# UIA doesn't implement word movement, so we need to do it manually.
# Relative to the current line, calculate our offset
Expand Down Expand Up @@ -128,98 +227,8 @@ def move(self, unit, direction, endPoint=None):
# after moving.
# Therefore manually collapse.
self.collapse()
# Console textRanges have access to the entire console buffer.
# However, we want to limit ourselves to onscreen text.
# Therefore, if the textInfo was originally visible,
# but we are now above or below the visible range,
# Restore the original textRange and pretend the move didn't work.
if oldInfo:
try:
if (
(
self.compareEndPoints(boundingInfo, "startToStart") < 0
or self.compareEndPoints(boundingInfo, "startToEnd") >= 0
)
and not (
oldInfo.compareEndPoints(boundingInfo, "startToStart") < 0
or oldInfo.compareEndPoints(boundingInfo, "startToEnd") >= 0
)
):
self._rangeObj = oldInfo._rangeObj
return 0
except (COMError, RuntimeError):
pass
return res

def expand(self, unit):
if unit == textInfos.UNIT_WORD:
# UIA doesn't implement word movement, so we need to do it manually.
lineInfo = self.copy()
lineInfo.expand(textInfos.UNIT_LINE)
offset = self._getCurrentOffsetInThisLine(lineInfo)
start, end = self._getWordOffsetsInThisLine(offset, lineInfo)
wordEndPoints = (
(offset - start) * -1,
end - offset - 1
)
if wordEndPoints[0]:
self._rangeObj.MoveEndpointByUnit(
UIAHandler.TextPatternRangeEndpoint_Start,
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
wordEndPoints[0]
)
if wordEndPoints[1]:
self._rangeObj.MoveEndpointByUnit(
UIAHandler.TextPatternRangeEndpoint_End,
UIAHandler.NVDAUnitsToUIAUnits[textInfos.UNIT_CHARACTER],
wordEndPoints[1]
)
else:
return super(consoleUIATextInfo, self).expand(unit)

def compareEndPoints(self, other, which):
"""Works around a UIA bug on Windows 10 1803 and later."""
# Even when a console textRange's start and end have been moved to the
# same position, the console incorrectly reports the end as being
# past the start.
# Compare to the start (not the end) when collapsed.
selfEndPoint, otherEndPoint = which.split("To")
if selfEndPoint == "end" and self._isCollapsed():
selfEndPoint = "start"
if otherEndPoint == "End" and other._isCollapsed():
otherEndPoint = "Start"
which = f"{selfEndPoint}To{otherEndPoint}"
return super().compareEndPoints(other, which=which)

def setEndPoint(self, other, which):
"""Override of L{textInfos.TextInfo.setEndPoint}.
Works around a UIA bug on Windows 10 1803 and later that means we can
not trust the "end" endpoint of a collapsed (empty) text range
for comparisons.
"""
selfEndPoint, otherEndPoint = which.split("To")
# In this case, there is no need to check if self is collapsed
# since the point of this method is to change its text range, modifying the "end" endpoint of a collapsed
# text range is fine.
if otherEndPoint == "End" and other._isCollapsed():
otherEndPoint = "Start"
which = f"{selfEndPoint}To{otherEndPoint}"
return super().setEndPoint(other, which=which)

def _isCollapsed(self):
"""Works around a UIA bug on Windows 10 1803 and later that means we
cannot trust the "end" endpoint of a collapsed (empty) text range
for comparisons.
Instead we check to see if we can get the first character from the
text range. A collapsed range will not have any characters
and will return an empty string."""
return not bool(self._rangeObj.getText(1))

def _get_isCollapsed(self):
# To decide if the textRange is collapsed,
# Check if it has no text.
return self._isCollapsed()

def _getCurrentOffsetInThisLine(self, lineInfo):
"""
Given a caret textInfo expanded to line, returns the index into the
Expand Down Expand Up @@ -258,18 +267,19 @@ def _getWordOffsetsInThisLine(self, offset, lineInfo):
min(end.value, max(1, lineTextLen - 2))
)

def __ne__(self, other):
"""Support more accurate caret move detection."""
return not self == other
def _isCollapsed(self):
"""Works around a UIA bug on Windows 10 1803 to 2004 that means we
cannot trust the "end" endpoint of a collapsed (empty) text range
for comparisons.
Instead we check to see if we can get the first character from the
text range. A collapsed range will not have any characters
and will return an empty string."""
return not bool(self._rangeObj.getText(1))

def _get_text(self):
# #10036: return a space if the text range is empty.
# Consoles don't actually store spaces, the character is merely left blank.
res = super(consoleUIATextInfo, self)._get_text()
if not res:
return ' '
else:
return res
def _get_isCollapsed(self):
# To decide if the textRange is collapsed,
# Check if it has no text.
return self._isCollapsed()


class consoleUIAWindow(Window):
Expand Down Expand Up @@ -302,7 +312,7 @@ def _get_TextInfo(self):
on NVDAObjects.UIA.UIA
consoleUIATextInfo fixes expand/collapse, implements word movement, and
bounds review to the visible text."""
return consoleUIATextInfo
return consoleUIATextInfo if isWin10(2004) else consoleUIATextInfoPre2004

def _getTextLines(self):
# This override of _getTextLines takes advantage of the fact that
Expand Down
23 changes: 22 additions & 1 deletion source/UIAUtils.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
# A part of NonVisual Desktop Access (NVDA)
# Copyright (C) 2015-2016 NV Access Limited
# Copyright (C) 2015-2020 NV Access Limited, Bill Dengler
# This file is covered by the GNU General Public License.
# See the file COPYING for more details.

import operator
from comtypes import COMError
import config
import ctypes
import UIAHandler
from winVersion import isWin10

def createUIAMultiPropertyCondition(*dicts):
"""
Expand Down Expand Up @@ -224,3 +226,22 @@ def getValue(self,ID,ignoreMixedValues=False):
if not ignoreMixedValues and val==UIAHandler.handler.ReservedMixedAttributeValue:
raise UIAMixedAttributeError
return val


def shouldUseUIAConsole(setting=None):
"""Determines whether to use UIA in the Windows Console.
@param setting: the config value to base this check on (if not provided,
it is retrieved from config).
"""
if not setting:
setting = config.conf['UIA']['winConsoleImplementation']
if setting == "legacy":
return False
elif setting == "UIA":
return True
# #7497: Windows 10 Fall Creators Update has an incomplete UIA
# implementation for console windows, therefore for now we should
# ignore it.
# It does not implement caret/selection, and probably has no
# new text events.
return isWin10(2004)
10 changes: 4 additions & 6 deletions source/_UIAHandler.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,12 +500,10 @@ def IUIAutomationNotificationEventHandler_HandleNotificationEvent(

def _isBadUIAWindowClassName(self, windowClass):
"Given a windowClassName, returns True if this is a known problematic UIA implementation."
# #7497: Windows 10 Fall Creators Update has an incomplete UIA
# implementation for console windows, therefore for now we should
# ignore it.
# It does not implement caret/selection, and probably has no new text
# events.
if windowClass == "ConsoleWindowClass" and config.conf['UIA']['winConsoleImplementation'] != "UIA":
if (
windowClass == "ConsoleWindowClass"
and not UIAUtils.shouldUseUIAConsole()
):
return True
return windowClass in badUIAWindowClassNames

Expand Down
Loading

0 comments on commit 1faa8e9

Please sign in to comment.