diff --git a/source/NVDAObjects/IAccessible/MSHTML.py b/source/NVDAObjects/IAccessible/MSHTML.py index 14def28b70b..dd71569cde3 100644 --- a/source/NVDAObjects/IAccessible/MSHTML.py +++ b/source/NVDAObjects/IAccessible/MSHTML.py @@ -27,6 +27,7 @@ from .. import InvalidNVDAObject from ..window import Window from NVDAObjects.UIA import UIA, UIATextInfo +from locationHelper import RectLTRB IID_IHTMLElement=comtypes.GUID('{3050F1FF-98B5-11CF-BB82-00AA00BDCE0B}') @@ -589,11 +590,7 @@ def _get_location(self): top=int(r.top*yFactor) right=int(r.right*xFactor) bottom=int(r.bottom*yFactor) - width=right-left - height=bottom-top - p=ctypes.wintypes.POINT(x=left,y=top) - ctypes.windll.user32.ClientToScreen(self.windowHandle,ctypes.byref(p)) - return (p.x,p.y,width,height) + return RectLTRB(left,top,right,bottom).toScreen(self.windowHandle).toLTWH() return None def _get_TextInfo(self): diff --git a/source/NVDAObjects/IAccessible/__init__.py b/source/NVDAObjects/IAccessible/__init__.py index 65894f4ff6c..ed5d6e34710 100644 --- a/source/NVDAObjects/IAccessible/__init__.py +++ b/source/NVDAObjects/IAccessible/__init__.py @@ -31,6 +31,7 @@ import NVDAObjects.JAB import eventHandler from NVDAObjects.behaviors import ProgressBar, Dialog, EditableTextWithAutoSelectDetection, FocusableUnfocusableContainer, ToolTip, Notification +from locationHelper import RectLTWH def getNVDAObjectFromEvent(hwnd,objectID,childID): try: @@ -902,7 +903,7 @@ def _get_childCount(self): def _get_location(self): try: - return self.IAccessibleObject.accLocation(self.IAccessibleChildID) + return RectLTWH(*self.IAccessibleObject.accLocation(self.IAccessibleChildID)) except COMError: return None diff --git a/source/NVDAObjects/IAccessible/sysListView32.py b/source/NVDAObjects/IAccessible/sysListView32.py index 8e090353b54..802e5f3a154 100644 --- a/source/NVDAObjects/IAccessible/sysListView32.py +++ b/source/NVDAObjects/IAccessible/sysListView32.py @@ -17,12 +17,12 @@ import api import eventHandler import winKernel -import winUser from . import IAccessible, List from ..window import Window import watchdog from NVDAObjects.behaviors import RowWithoutCellObjects, RowWithFakeNavigation import config +from locationHelper import RectLTRB #Window messages LVM_FIRST=0x1000 @@ -53,6 +53,15 @@ LVIF_GROUPID=0x100 LVIF_COLUMNS=0x200 +#GETSUBITEMRECT flags +# Returns the bounding rectangle of the entire item, including the icon and label +LVIR_BOUNDS = 0 +# Returns the bounding rectangle of the icon or small icon. +LVIR_ICON = 1 +# Returns the bounding rectangle of the entire item, including the icon and label. +# This is identical to LVIR_BOUNDS. +LVIR_LABEL = 2 + #Item states LVIS_FOCUSED=0x01 LVIS_SELECTED=0x02 @@ -297,8 +306,13 @@ def event_stateChange(self): class ListItem(RowWithFakeNavigation, RowWithoutCellObjects, ListItemWithoutColumnSupport): def _getColumnLocationRaw(self,index): + assert index>0, "Invalid index: %d" % index processHandle=self.processHandle - localRect=RECT(left=2,top=index) + # LVM_GETSUBITEMRECT requires a pointer to a RECT structure that will receive the subitem bounding rectangle information. + localRect=RECT( + left=LVIR_LABEL, # Returns the bounding rectangle of the entire item, including the icon and label + top=index # The one-based index of the subitem + ) internalRect=winKernel.virtualAllocEx(processHandle,None,sizeof(localRect),winKernel.MEM_COMMIT,winKernel.PAGE_READWRITE) try: winKernel.writeProcessMemory(processHandle,internalRect,byref(localRect),sizeof(localRect),None) @@ -306,9 +320,13 @@ def _getColumnLocationRaw(self,index): winKernel.readProcessMemory(processHandle,internalRect,byref(localRect),sizeof(localRect),None) finally: winKernel.virtualFreeEx(processHandle,internalRect,0,winKernel.MEM_RELEASE) - windll.user32.ClientToScreen(self.windowHandle,byref(localRect)) - windll.user32.ClientToScreen(self.windowHandle,byref(localRect,8)) - return (localRect.left,localRect.top,localRect.right-localRect.left,localRect.bottom-localRect.top) + # #8268: this might be a malformed rectangle + # (i.e. with a left coordinate that is greather than the right coordinate). + left = min(localRect.left, localRect.right) + top = min(localRect.top, localRect.bottom) + right = max(localRect.left, localRect.right) + bottom = max(localRect.top, localRect.bottom) + return RectLTRB(left, top, right, bottom).toScreen(self.windowHandle).toLTWH() def _getColumnLocation(self,column): return self._getColumnLocationRaw(self.parent._columnOrderArray[column - 1]) diff --git a/source/NVDAObjects/JAB/__init__.py b/source/NVDAObjects/JAB/__init__.py index 23bc1e641d1..51130f8dc96 100644 --- a/source/NVDAObjects/JAB/__init__.py +++ b/source/NVDAObjects/JAB/__init__.py @@ -9,6 +9,7 @@ import textInfos.offsets from logHandler import log from .. import InvalidNVDAObject +from locationHelper import RectLTWH JABRolesToNVDARoles={ "alert":controlTypes.ROLE_DIALOG, @@ -301,7 +302,7 @@ def _get_description(self): return re_simpleXmlTag.sub(" ", self._JABAccContextInfo.description) def _get_location(self): - return (self._JABAccContextInfo.x,self._JABAccContextInfo.y,self._JABAccContextInfo.width,self._JABAccContextInfo.height) + return RectLTWH(self._JABAccContextInfo.x,self._JABAccContextInfo.y,self._JABAccContextInfo.width,self._JABAccContextInfo.height) def _get_hasFocus(self): if controlTypes.STATE_FOCUSED in self.states: diff --git a/source/NVDAObjects/UIA/__init__.py b/source/NVDAObjects/UIA/__init__.py index caec534ca58..3730a78a39e 100644 --- a/source/NVDAObjects/UIA/__init__.py +++ b/source/NVDAObjects/UIA/__init__.py @@ -31,6 +31,7 @@ from NVDAObjects.behaviors import ProgressBar, EditableTextWithoutAutoSelectDetection, Dialog, Notification, EditableTextWithSuggestions import braille import time +from locationHelper import RectLTWH import ui class UIATextInfo(textInfos.TextInfo): @@ -1296,12 +1297,7 @@ def _get_location(self): if r is None: return # r is a tuple of floats representing left, top, width and height. - # However, most NVDA code expecs location coordinates to be ints - left=int(r[0]) - top=int(r[1]) - width=int(r[2]) - height=int(r[3]) - return left,top,width,height + return RectLTWH.fromFloatCollection(*r) def _get_value(self): val=self._getUIACacheablePropertyValue(UIAHandler.UIA_RangeValueValuePropertyId,True) diff --git a/source/NVDAObjects/window/__init__.py b/source/NVDAObjects/window/__init__.py index d2e6f8b1183..2d22d84084a 100644 --- a/source/NVDAObjects/window/__init__.py +++ b/source/NVDAObjects/window/__init__.py @@ -17,6 +17,7 @@ from NVDAObjects import NVDAObject from NVDAObjects.behaviors import EditableText, LiveText import watchdog +from locationHelper import RectLTWH re_WindowsForms=re.compile(r'^WindowsForms[0-9]*\.(.*)\.app\..*$') re_ATL=re.compile(r'^ATL:(.*)$') @@ -192,7 +193,7 @@ def _get_windowControlID(self): def _get_location(self): r=ctypes.wintypes.RECT() ctypes.windll.user32.GetWindowRect(self.windowHandle,ctypes.byref(r)) - return (r.left,r.top,r.right-r.left,r.bottom-r.top) + return RectLTWH.fromCompatibleType(r) def _get_displayText(self): """The text at this object's location according to the display model for this object's window.""" diff --git a/source/appModules/powerpnt.py b/source/appModules/powerpnt.py index 7aea222cfbd..43bb8bc53b9 100644 --- a/source/appModules/powerpnt.py +++ b/source/appModules/powerpnt.py @@ -33,6 +33,7 @@ import controlTypes from logHandler import log import scriptHandler +from locationHelper import RectLTRB from NVDAObjects.window._msOfficeChart import OfficeChart # Window classes where PowerPoint's object model should be used @@ -738,9 +739,7 @@ def _get_location(self): top=self.documentWindow.ppObjectModel.pointsToScreenPixelsY(pointTop) right=self.documentWindow.ppObjectModel.pointsToScreenPixelsX(pointLeft+pointWidth) bottom=self.documentWindow.ppObjectModel.pointsToScreenPixelsY(pointTop+pointHeight) - width=right-left - height=bottom-top - return (left,top,width,height) + return RectLTRB(left,top,right,bottom).toLTWH() def _get_ppShapeType(self): """Fetches and caches the type of this shape.""" diff --git a/source/displayModel.py b/source/displayModel.py index f398fcd3206..09f20fd2121 100644 --- a/source/displayModel.py +++ b/source/displayModel.py @@ -19,6 +19,7 @@ import watchdog from logHandler import log import windowUtils +from locationHelper import RectLTRB def wcharToInt(c): i=ord(c) @@ -178,7 +179,7 @@ def getWindowTextInRect(bindingHandle, windowHandle, left, top, right, bottom,mi characterLocations = [] cpBufIt = iter(cpBuf) for cp in cpBufIt: - characterLocations.append((wcharToInt(cp), wcharToInt(next(cpBufIt)), wcharToInt(next(cpBufIt)), wcharToInt(next(cpBufIt)))) + characterLocations.append(RectLTRB(wcharToInt(cp), wcharToInt(next(cpBufIt)), wcharToInt(next(cpBufIt)), wcharToInt(next(cpBufIt)))) return text, characterLocations def getFocusRect(obj): diff --git a/source/locationHelper.py b/source/locationHelper.py new file mode 100644 index 00000000000..11dc55f45eb --- /dev/null +++ b/source/locationHelper.py @@ -0,0 +1,411 @@ +#locationHelper.py +#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) 2017-2018 NV Access Limited, Babbage B.V. + +"""Classes and helper functions for working with rectangles and coordinates.""" + +from collections import namedtuple, Sequence +import windowUtils +import winUser +from ctypes.wintypes import RECT, POINT, DWORD +import textInfos +import wx + +class Point(namedtuple("Point",("x","y"))): + """Represents a point on the screen.""" + + @classmethod + def fromFloatCollection(cls, *floats): + """Creates an instance from float parameters. + The provided parameters will be converted to ints automatically. + @raise TypeError: If one of the input parameters isn't a float. + """ + if not all(isinstance(f, float) for f in floats): + raise TypeError("All input parameters must be of type float") + return cls(*map(int, floats)) + + @classmethod + def fromCompatibleType(cls, point): + """Creates an instance from a compatible type. + Compatible types are defined in L{POINT_CLASSES}. + """ + if isinstance(point,POINT_CLASSES): + return cls(point.x, point.y) + raise TypeError("point should be one of %s" % ", ".join(cls.__module__+"."+cls.__name__ for cls in POINT_CLASSES)) + + @classmethod + def fromDWORD(cls, dwPoint): + if isinstance(dwPoint,DWORD): + dwPoint = dwPoint.value + if not isinstance(dwPoint,(int,long)): + raise TypeError("dwPoint should be one of int, long or ctypes.wintypes.DWORD (ctypes.ulong)") + return Point(winUser.GET_X_LPARAM(dwPoint),winUser.GET_Y_LPARAM(dwPoint)) + + def __add__(self,other): + """Returns a new L{Point} with its coordinates representing the additions of the original x and y coordinates.""" + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return Point((self.x+other.x),(self.y+other.y)) + + def __radd__(self,other): + """Returns a new L{Point} with x = self.x + other.x and y = self.y + other.y.""" + if other == 0: + return self + elif not isinstance(other,POINT_CLASSES): + return NotImplemented + return Point((other.x+self.x),(other.y+self.y)) + + def __sub__(self,other): + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return Point((self.x-other.x),(self.y-other.y)) + + def __rsub__(self,other): + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return Point((other.x-self.x),(other.y-self.y)) + + def yWiseLessThan(self,other): + """ + Returns whether self is less than other, first comparing y, then x coordinates. + For example: (x=4,y=3) < (x=3,y=4) because self.y is less than other.y. + To compare in opposite order (i.e. compare x, then y), use L{xWiseLessThan} + """ + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return (self.y, self.x) < (other.y, other.x) + + def xWiseLessThan(self,other): + """ + Returns whether self is less than other, first comparing x, then y coordinates. + For example: (x=3,y=4) < (x=4,y=3) because self.x is less than other.x. + To compare in opposite order (i.e. compare y, then x), use L{yWiseLessThan} + """ + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return (self.x, self.y) < (other.x, other.y) + + def yWiseLessOrEq(self,other): + """ + Returns whether self is less than or equal to other, first comparing y, then x coordinates. + For example: (x=4,y=3) <= (x=3,y=4) because self.y is less than or equal to other.y. + To compare in opposite order (i.e. compare x, then y), use L{xWiseLessOrEq} + """ + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return (self.y, self.x) <= (other.y, other.x) + + def xWiseLessOrEq(self,other): + """ + Returns whether self is less than or equal to other, first comparing x, then y coordinates. + For example: (x=3,y=4) <= (x=4,y=3) because self.x is less than or equal to other.x. + To compare in opposite order (i.e. compare y, then x), use L{yWiseLessOrEq} + """ + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return (self.x, self.y) <= (other.x, other.y) + + def yWiseGreaterThan(self,other): + """ + Returns whether self is greater than other, first comparing y, then x coordinates. + For example: (x=3,y=4) > (x=4,y=3) because self.y is greater than other.y. + To compare in opposite order (i.e. compare x, then y), use L{xWiseGreaterThan} + """ + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return (self.y, self.x) > (other.y, other.x) + + def xWiseGreaterThan(self,other): + """ + Returns whether self is greater than other, first comparing x, then y coordinates. + For example: (x=4,y=3) > (x=3,y=4) because self.x is greater than other.x. + To compare in opposite order (i.e. compare y, then x), use L{yWiseGreaterThan} + """ + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return (self.x, self.y) > (other.x, other.y) + + def yWiseGreaterOrEq(self,other): + """ + Returns whether self is greater than or equal to other, first comparing y, then x coordinates. + For example: (x=3,y=4) >= (x=4,y=3) because self.y is greater than or equal to other.y. + To compare in opposite order (i.e. compare x, then y), use L{xWiseGreaterOrEq} + """ + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return (self.y, self.x) >= (other.y, other.x) + + def xWiseGreaterOrEq(self,other): + """ + Returns whether self is greater than or equal to other, first comparing x, then y coordinates. + For example: (x=4,y=3) >= (x=3,y=4) because self.x is greater than or equal to other.x. + To compare in opposite order (i.e. compare y, then x), use L{yWiseGreaterOrEq} + """ + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return (self.x, self.y) >= (other.x, other.y) + + def __eq__(self,other): + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return self.x == other.x and self.y == other.y + + def __ne__(self,other): + if not isinstance(other,POINT_CLASSES): + return NotImplemented + return self.x != other.x or self.y != other.y + + def toPOINT(self): + """Converts self to a L{ctypes.wintypes.POINT}""" + return POINT(self.x,self.y) + + def toLogical(self, hwnd): + """Converts self from physical to logical coordinates and returns a new L{Point}.""" + return Point(*windowUtils.physicalToLogicalPoint(hwnd, *self)) + + def toPhysical(self, hwnd): + """Converts self from logical to physical coordinates and returns a new L{Point}""" + return Point(*windowUtils.logicalToPhysicalPoint(hwnd, *self)) + + def toClient(self, hwnd): + """Converts self from screen to client coordinates and returns a new L{Point}""" + return Point(*winUser.ScreenToClient(hwnd, *self)) + + def toScreen(self, hwnd): + """Converts self from client to screen coordinates and returns a new L{Point}""" + return Point(*winUser.ClientToScreen(hwnd, *self)) + +class _RectMixin: + """Mix-in class for properties shared between RectLTRB and RectLTWH classes""" + + @classmethod + def fromFloatCollection(cls, *floats): + """Creates an instance from float parameters. + The provided parameters will be converted to ints automatically. + @raise TypeError: If one of the input parameters isn't a float. + """ + if not all(isinstance(f, float) for f in floats): + raise TypeError("All input parameters must be of type float") + return cls(*map(int, floats)) + + @classmethod + def fromCompatibleType(cls, rect): + """Creates an instance from a compatible type. + Compatible types are defined in L{RECT_CLASSES}. + """ + if isinstance(rect,cls): + return rect + if isinstance(rect,RECT_CLASSES): + if cls is RectLTWH: + return cls(rect.left, rect.top, rect.right-rect.left, rect.bottom-rect.top) + elif cls is RectLTRB: + return cls(rect.left, rect.top, rect.right, rect.bottom) + raise TypeError("rect should be one of %s" % ", ".join(cls.__module__+"."+cls.__name__ for cls in RECT_CLASSES)) + + @classmethod + def fromPoint(cls, point): + """Creates an instance from a compatible point type with a width and height of 0.""" + if isinstance(point,POINT_CLASSES): + if cls is RectLTWH: + return cls(point.x, point.y, 0, 0) + elif cls is RectLTRB: + return cls(point.x, point.y, point.x, point.y) + else: + raise RuntimeError("%s is not known as a valid subclass of _RectMixin" % cls.__name__) + raise TypeError("point should be one of %s" % ", ".join(cls.__module__+"."+cls.__name__ for cls in POINT_CLASSES)) + + @classmethod + def fromCollection(cls, *items): + """Creates a bounding rectangle for the provided collection of items. + The highest coordinates found in the collection are considered exclusive. + For example, if you provide Point(x=1,y=1) and point(x=2,y=2), + The resulting rectangle coordinates are left=1,top=1,right=2,bottom=2. + Input could be of mixed types from either L{RECT_CLASSES} or L{POINT_CLASSES}. + """ + if len(items)==0: + raise TypeError("This function takes at least 1 argument (0 given)") + xs=set() + ys=set() + for item in items: + if isinstance(item,RECT_CLASSES): + xs.update((item.left,item.right)) + ys.update((item.top,item.bottom)) + elif isinstance(item,POINT_CLASSES): + xs.add(item.x) + ys.add(item.y) + else: + raise ValueError("Unexpected parameter %s"%str(item)) + left=min(xs) + top=min(ys) + right=max(xs) + bottom=max(ys) + if cls is RectLTWH: + return cls(left, top, right-left, bottom-top) + return cls(left, top, right, bottom) + + def toRECT(self): + """Converts self to a L{ctypes.wintypes.RECT}""" + return RECT(self.left,self.top,self.right,self.bottom) + + def toLogical(self, hwnd): + left,top=self.topLeft.toLogical(hwnd) + right,bottom=self.bottomRight.toLogical(hwnd) + if isinstance(self, RectLTWH): + return RectLTWH(left,top,right-left,bottom-top) + return RectLTRB(left,top,right,bottom) + + def toPhysical(self, hwnd): + left,top=self.topLeft.toPhysical(hwnd) + right,bottom=self.bottomRight.toPhysical(hwnd) + if isinstance(self, RectLTWH): + return RectLTWH(left,top,right-left,bottom-top) + return RectLTRB(left,top,right,bottom) + + def toClient(self, hwnd): + left, top =self.topLeft.toClient(hwnd) + if isinstance(self, RectLTWH): + return RectLTWH(left, top, self.width, self.height) + return RectLTRB(left, top, left+self.width, top+self.height) + + def toScreen(self, hwnd): + left,top=self.topLeft.toScreen(hwnd) + if isinstance(self, RectLTWH): + return RectLTWH(left, top, self.width, self.height) + return RectLTRB(left, top, left+self.width, top+self.height) + + @property + def topLeft(self): + return Point(self.left,self.top) + + @property + def topRight(self): + return Point(self.right,self.top) + + @property + def bottomLeft(self): + return Point(self.left,self.bottom) + + @property + def bottomRight(self): + return Point(self.right,self.bottom) + + @property + def center(self): + return Point(int(round(self.left+self.width/2.0)), int(round(self.top+self.height/2.0))) + + def __contains__(self,other): + """Returns whether other is a part of this rectangle.""" + if isinstance(other,POINT_CLASSES): + return self.left <= other.x < self.right and self.top <= other.y < self.bottom + if not isinstance(other,RECT_CLASSES): + return NotImplemented + return self.isSuperset(other) and self!=other + + def isSubset(self,other): + """Returns whether this rectangle is a subset of other (i.e. whether all points in this rectangle are contained by other).""" + if not isinstance(other,RECT_CLASSES): + return False + return other.left<=self.left<=self.right<=other.right and other.top<=self.top<=self.bottom<=other.bottom + + def isSuperset(self,other): + """Returns whether this rectangle is a superset of other (i.e. whether all points of other are contained by this rectangle).""" + if not isinstance(other,RECT_CLASSES): + raise TypeError("other should be one of %s" % ", ".join(cls.__module__+"."+cls.__name__ for cls in RECT_CLASSES)) + return self.left<=other.left<=other.right<=self.right and self.top<=other.top<=other.bottom<=self.bottom + + def __eq__(self,other): + if not isinstance(other,RECT_CLASSES): + return NotImplemented + return other.left == self.left and other.top == self.top and other.right == self.right and other.bottom == self.bottom + + def __ne__(self,other): + if not isinstance(other,RECT_CLASSES): + return NotImplemented + return other.left != self.left or other.top != self.top or other.right != self.right or other.bottom != self.bottom + + def intersection(self,other): + """Returns the intersection of self and other. + For example, if self = Rect(left=10,top=10,right=25,bottom=25) and other = Rect(left=20,top=20,right=35,bottom=35), + this results in Rect(left=20,top=20,right=25,bottom=25). + No intersect results in a rectangle with zeroed coordinates. + """ + if not isinstance(other,RECT_CLASSES): + raise TypeError("other should be one of %s" % ", ".join(cls.__module__+"."+cls.__name__ for cls in RECT_CLASSES)) + left=max(self.left,other.left) + top=max(self.top,other.top) + right=min(self.right,other.right) + bottom=min(self.bottom,other.bottom) + if left>right or top>bottom: + left=top=right=bottom=0 + if isinstance(self, RectLTWH): + return RectLTWH(left,top,right-left,bottom-top) + return RectLTRB(left,top,right,bottom) + + def expandOrShrink(self, margin): + """Expands or shrinks the boundaries of the rectangle with the given margin. + For example, if self = Rect(left=10,top=10,right=25,bottom=25) and margin = 10, + this results in Rect(left=0,top=0,right=35,bottom=35). + If self = Rect(left=10,top=10,right=25,bottom=25) and margin = -5, + this results in Rect(left=15,top=15,right=20,bottom=20). + """ + if not isinstance(margin, int): + raise TypeError("Margin should be an integer") + left=self.left-margin + top=self.top-margin + right=self.right+margin + bottom=self.bottom+margin + if left>right or top>bottom: + raise RuntimeError("The provided margin of %d would result in a rectangle with a negative width or height, which is not allowed"%margin) + if isinstance(self, RectLTWH): + return RectLTWH(left,top,right-left,bottom-top) + return RectLTRB(left,top,right,bottom) + +class RectLTWH(_RectMixin, namedtuple("RectLTWH",("left","top","width","height"))): + """ + Represents a rectangle on the screen, based on left and top coordinates, width and height. + To represent a rectangle using left, top, right and bottom coordinates, use L{RectLTRB}. + """ + + @property + def right(self): + return self.left+self.width + + @property + def bottom(self): + return self.top+self.height + + def toLTRB(self): + return RectLTRB(self.left,self.top,self.right,self.bottom) + +class RectLTRB(_RectMixin, namedtuple("RectLTRB",("left","top","right","bottom"))): + """Represents a rectangle on the screen. + By convention, the right and bottom edges of the rectangle are normally considered exclusive. + To represent a rectangle based on width and height instead, use L{RectLTWH}. + """ + + def __new__(cls, left, top, right, bottom): + if left>right: + raise ValueError("left=%d is greather than right=%d, which is not allowed"%(left,right)) + if top>bottom: + raise ValueError("top=%d is greather than bottom=%d, which is not allowed"%(top,bottom)) + return super(RectLTRB, cls).__new__(cls, left, top, right, bottom) + + @property + def width(self): + return self.right-self.left + + @property + def height(self): + return self.bottom-self.top + + def toLTWH(self): + return RectLTWH(self.left,self.top,self.width,self.height) + +#: Classes which support conversion to locationHelper Points using their x and y properties. +#: type: tuple +POINT_CLASSES=(Point, POINT, textInfos.Point, wx.Point) +#: Classes which support conversion to locationHelper RectLTRB/LTWH using their left, top, right and bottom properties. +#: type: tuple +RECT_CLASSES=(RectLTRB, RectLTWH, RECT, textInfos.Rect) diff --git a/source/textInfos/__init__.py b/source/textInfos/__init__.py index 3b01234a958..a219a059d9f 100755 --- a/source/textInfos/__init__.py +++ b/source/textInfos/__init__.py @@ -161,6 +161,7 @@ def __repr__(self): class Point(object): """Represents a point on the screen. This is used when associating a point on the screen with a piece of text. + @Deprecated: use L{locationHelper.Point} instead. """ def __init__(self,x,y): @@ -174,7 +175,8 @@ def __init__(self,x,y): self.y=y class Rect(object): - """Represents a rectangle on the screen.""" + """Represents a rectangle on the screen. + @Deprecated: use L{locationHelper.Rect} instead.""" def __init__(self, left, top, right, bottom): """ diff --git a/source/winUser.py b/source/winUser.py index 1686da53b58..d9d9b0d321b 100644 --- a/source/winUser.py +++ b/source/winUser.py @@ -536,6 +536,12 @@ def ScreenToClient(hwnd, x, y): user32.ScreenToClient(hwnd, byref(point)) return point.x, point.y +def ClientToScreen(hwnd, x, y): + point = POINT(x, y) + user32.ClientToScreen(hwnd, byref(point)) + return point.x, point.y + + class STICKYKEYS(Structure): _fields_ = ( ("cbSize", DWORD), diff --git a/tests/unit/test_locationHelper.py b/tests/unit/test_locationHelper.py new file mode 100644 index 00000000000..c14c8e11fd2 --- /dev/null +++ b/tests/unit/test_locationHelper.py @@ -0,0 +1,195 @@ +#tests/unit/test_locationHelper.py +#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) 2017 NV Access Limited, Babbage B.V. + +"""Unit tests for the locationHelper module. +""" + +import unittest +from locationHelper import * +from ctypes.wintypes import RECT, POINT + +class TestRectOperators(unittest.TestCase): + + def test_intersection(self): + self.assertEqual(RectLTRB(left=2, top=2, right=4, bottom=4).intersection(RectLTRB(left=3, top=3, right=5, bottom=5)), RectLTRB(left=3, top=3, right=4, bottom=4)) + self.assertEqual(RectLTRB(left=2, top=2, right=4, bottom=4).intersection(RectLTRB(left=5, top=5, right=7, bottom=7)), RectLTRB(left=0, top=0, right=0, bottom=0)) + + def test_superset(self): + self.assertTrue(RectLTRB(left=2, top=2, right=6, bottom=6).isSuperset(RectLTRB(left=2, top=2, right=4, bottom=4))) + self.assertFalse(RectLTRB(left=2, top=2, right=4, bottom=4).isSuperset(RectLTRB(left=2, top=2, right=6, bottom=6))) + self.assertTrue(RectLTRB(left=2, top=2, right=6, bottom=6).isSuperset(RectLTRB(left=2, top=2, right=6, bottom=6))) + + def test_subset(self): + self.assertTrue(RectLTRB(left=2, top=2, right=4, bottom=4).isSubset(RectLTRB(left=2, top=2, right=6, bottom=6))) + self.assertFalse(RectLTRB(left=2, top=2, right=6, bottom=6).isSubset(RectLTRB(left=2, top=2, right=4, bottom=4))) + self.assertTrue(RectLTRB(left=2, top=2, right=6, bottom=6).isSubset(RectLTRB(left=2, top=2, right=6, bottom=6))) + + def test_in(self): + rect = RectLTRB(left=2, top=2, right=6, bottom=6) + self.assertIn(RectLTRB(left=2, top=2, right=4, bottom=4), rect) + self.assertNotIn(rect, rect) + self.assertIn(Point(x=2, y=2), rect) + self.assertIn(Point(x=4, y=4), rect) + self.assertNotIn(Point(x=2, y=6), rect) + self.assertNotIn(Point(x=6, y=2), rect) + self.assertNotIn(Point(x=6, y=6), rect) + + def test_equal(self): + self.assertEqual(RectLTRB(left=2, top=2, right=4, bottom=4), RectLTRB(left=2, top=2, right=4, bottom=4)) + self.assertNotEqual(RectLTRB(left=2, top=2, right=4, bottom=4), RectLTRB(left=2, top=2, right=6, bottom=6)) + self.assertEqual(RectLTRB(left=2, top=2, right=4, bottom=4), RectLTWH(left=2, top=2, width=2, height=2)) + self.assertNotEqual(RectLTRB(left=2, top=2, right=4, bottom=4), RectLTWH(left=2, top=2, width=4, height=4)) + + def test_ctypesRECT(self): + # Intersection + self.assertEqual(RectLTRB(left=2, top=2, right=4, bottom=4).intersection(RECT(left=3, top=3, right=5, bottom=5)), RectLTRB(left=3, top=3, right=4, bottom=4)) + # Superset + self.assertTrue(RectLTRB(left=2, top=2, right=6, bottom=6).isSuperset(RECT(left=2, top=2, right=4, bottom=4))) + # Subset + self.assertTrue(RectLTRB(left=2, top=2, right=4, bottom=4).isSubset(RECT(left=2, top=2, right=6, bottom=6))) + # in + self.assertIn(RECT(left=2, top=2, right=4, bottom=4), RectLTRB(left=2, top=2, right=6, bottom=6)) + self.assertNotIn(RECT(left=2, top=2, right=4, bottom=4), RectLTRB(left=2, top=2, right=4, bottom=4)) + # Equality + self.assertEqual(RECT(left=2, top=2, right=4, bottom=4), RectLTRB(left=2, top=2, right=4, bottom=4)) + self.assertNotEqual(RECT(left=2, top=2, right=4, bottom=4), RectLTRB(left=2, top=2, right=6, bottom=6)) + +class TestRectUtilities(unittest.TestCase): + + def test_points(self): + rect = RectLTRB(left=-5, top=-5, right=5, bottom=5) + self.assertEqual(rect.topLeft, Point(x=-5, y=-5)) + self.assertEqual(rect.topRight, Point(x=5, y=-5)) + self.assertEqual(rect.bottomLeft, Point(x=-5, y=5)) + self.assertEqual(rect.bottomRight, Point(x=5, y=5)) + self.assertEqual(rect.center, Point(x=0, y=0)) + # Specifically test some other edge cases for center + self.assertEqual(RectLTRB(left=10, top=10, right=20, bottom=20).center, Point(x=15, y=15)) + self.assertEqual(RectLTRB(left=-20, top=-20, right=-10, bottom=-10).center, Point(x=-15, y=-15)) + self.assertEqual(RectLTRB(left=10, top=10, right=21, bottom=21).center, Point(x=16, y=16)) + self.assertEqual(RectLTRB(left=-21, top=-21, right=-10, bottom=-10).center, Point(x=-16, y=-16)) + + def test_collection(self): + """Tests whether a collection of several rectangle and point types convert to the expected L{RectLTRB}.""" + rect=RectLTRB(left=10, top=15, right=500, bottom=1000) + self.assertEqual(RectLTRB.fromCollection( + rect.topLeft, + rect.bottomRight, + rect.center, + Point(15, 15), + Point(20, 20), + Point(50, 50), + Point(400, 400), + POINT(x=15, y=15), + POINT(x=20, y=20), + POINT(x=50, y=50), + POINT(x=400, y=400), + RectLTRB(left=450, top=450, right=490, bottom=990), + RECT(450, 450, 490, 990) + ), rect) + + location=RectLTWH(left=10, top=15, width=500, height=1000) + self.assertEqual(RectLTWH.fromCollection( + location.topLeft, + location.bottomRight, + location.center, + Point(15, 15), + Point(20, 20), + Point(50, 50), + Point(400, 400), + POINT(x=15, y=15), + POINT(x=20, y=20), + POINT(x=50, y=50), + POINT(x=400, y=400), + RectLTRB(left=450, top=450, right=505, bottom=1010), + RECT(450, 450, 490, 990) + ), location) + + def test_fromFloatCollection(self): + self.assertEqual(RectLTRB(left=10, top=10, right=20, bottom=20), RectLTRB.fromFloatCollection(10.0, 10.0, 20.0, 20.0)) + self.assertEqual(RectLTWH(left=10, top=10, width=20, height=20), RectLTWH.fromFloatCollection(10.0, 10.0, 20.0, 20.0)) + + def test_valueErrorForUnsuportedInput(self): + self.assertRaises(ValueError, RectLTRB, left=10, top=10, right=9, bottom=9) + + def test_expandOrShrink(self): + """Tests the expand or shrink functionality to resize a rectangle given a specified margin.""" + rect = RectLTRB(left=10, top=10, right=20, bottom=20) + self.assertEqual(rect.expandOrShrink(10), RectLTRB(left=0, top=0, right=30, bottom=30)) + self.assertEqual(rect.expandOrShrink(-2), RectLTRB(left=12, top=12, right=18, bottom=18)) + self.assertRaises(RuntimeError, rect.expandOrShrink, -10) + + location = RectLTWH(left=10, top=10, width=10, height=10) + self.assertEqual(location.expandOrShrink(10), RectLTWH(left=0, top=0, width=30, height=30)) + self.assertEqual(location.expandOrShrink(-2), RectLTWH(left=12, top=12, width=6, height=6)) + self.assertRaises(RuntimeError, location.expandOrShrink, -10) + +class TestPointOperators(unittest.TestCase): + + def test_add(self): + self.assertEqual(Point(x=2, y=4)+Point(x=2, y=4),Point(x=4, y=8)) + + def test_sum(self): + point=Point(x=2, y=4) + self.assertEqual(sum((point, point, point)), Point(x=6, y=12)) + + def test_sub(self): + self.assertEqual(Point(x=2, y=4)-Point(x=4, y=8),Point(x=-2, y=-4)) + + def test_greaterThan(self): + self.assertTrue(Point(x=3, y=4).yWiseGreaterThan(Point(x=4, y=3))) + self.assertFalse(Point(x=3, y=4).xWiseGreaterThan(Point(x=4, y=3))) + self.assertTrue(Point(x=4, y=3).xWiseGreaterThan(Point(x=3, y=4))) + self.assertFalse(Point(x=4, y=3).yWiseGreaterThan(Point(x=3, y=4))) + self.assertTrue(Point(x=3, y=4).yWiseGreaterOrEq(Point(x=4, y=3))) + self.assertFalse(Point(x=3, y=4).xWiseGreaterOrEq(Point(x=4, y=3))) + self.assertTrue(Point(x=4, y=3).xWiseGreaterOrEq(Point(x=3, y=4))) + self.assertFalse(Point(x=4, y=3).yWiseGreaterOrEq(Point(x=3, y=4))) + + def test_lessThan(self): + self.assertTrue(Point(x=4, y=3).yWiseLessThan(Point(x=3, y=4))) + self.assertFalse(Point(x=4, y=3).xWiseLessThan(Point(x=3, y=4))) + self.assertTrue(Point(x=3, y=4).xWiseLessThan(Point(x=4, y=3))) + self.assertFalse(Point(x=3, y=4).yWiseLessThan(Point(x=4, y=3))) + self.assertTrue(Point(x=4, y=3).yWiseLessOrEq(Point(x=3, y=4))) + self.assertFalse(Point(x=4, y=3).xWiseLessOrEq(Point(x=3, y=4))) + self.assertTrue(Point(x=3, y=4).xWiseLessOrEq(Point(x=4, y=3))) + self.assertFalse(Point(x=3, y=4).yWiseLessOrEq(Point(x=4, y=3))) + + def test_equal(self): + self.assertEqual(Point(x=4, y=3), Point(x=4, y=3)) + self.assertNotEqual(Point(x=3, y=4), Point(x=4, y=3)) + + def test_ctypesPOINT(self): + # Add + self.assertEqual(Point(x=2, y=4)+POINT(x=2, y=4),Point(x=4, y=8)) + self.assertEqual(POINT(x=2, y=4)+Point(x=2, y=4),Point(x=4, y=8)) + # Sum + self.assertEqual(sum((Point(x=2, y=4), POINT(x=2, y=4), Point(x=2, y=4))), Point(x=6, y=12)) + # Subtract + self.assertEqual(Point(x=2, y=4)-POINT(x=4, y=8),Point(x=-2, y=-4)) + self.assertEqual(POINT(x=2, y=4)-Point(x=4, y=8),Point(x=-2, y=-4)) + # Greater than + self.assertTrue(Point(x=3, y=4).yWiseGreaterThan(POINT(x=4, y=3))) + self.assertFalse(Point(x=3, y=4).xWiseGreaterThan(POINT(x=4, y=3))) + self.assertTrue(Point(x=4, y=3).xWiseGreaterThan(POINT(x=3, y=4))) + self.assertFalse(Point(x=4, y=3).yWiseGreaterThan(POINT(x=3, y=4))) + self.assertTrue(Point(x=3, y=4).yWiseGreaterOrEq(POINT(x=4, y=3))) + self.assertFalse(Point(x=3, y=4).xWiseGreaterOrEq(POINT(x=4, y=3))) + self.assertTrue(Point(x=4, y=3).xWiseGreaterOrEq(POINT(x=3, y=4))) + self.assertFalse(Point(x=4, y=3).yWiseGreaterOrEq(POINT(x=3, y=4))) + # Less than + self.assertTrue(Point(x=4, y=3).yWiseLessThan(POINT(x=3, y=4))) + self.assertFalse(Point(x=4, y=3).xWiseLessThan(POINT(x=3, y=4))) + self.assertTrue(Point(x=3, y=4).xWiseLessThan(POINT(x=4, y=3))) + self.assertFalse(Point(x=3, y=4).yWiseLessThan(POINT(x=4, y=3))) + self.assertTrue(Point(x=4, y=3).yWiseLessOrEq(POINT(x=3, y=4))) + self.assertFalse(Point(x=4, y=3).xWiseLessOrEq(POINT(x=3, y=4))) + self.assertTrue(Point(x=3, y=4).xWiseLessOrEq(POINT(x=4, y=3))) + self.assertFalse(Point(x=3, y=4).yWiseLessOrEq(POINT(x=4, y=3))) + # Equality + self.assertEqual(POINT(x=4, y=3), Point(x=4, y=3)) + self.assertNotEqual(POINT(x=3, y=4), Point(x=4, y=3)) \ No newline at end of file