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