from ctypes import * from ctypes.wintypes import RECT from comtypes import BSTR import unicodedata import math import colors import XMLFormatting import api import winUser import NVDAHelper import textInfos from textInfos.offsets import OffsetsTextInfo import watchdog from logHandler import log import windowUtils def detectStringDirection(s): direction=0 for b in (unicodedata.bidirectional(ch) for ch in s): if b=='L': direction+=1 if b in ('R','AL'): direction-=1 return direction def normalizeRtlString(s): l=[] for c in s: #If this is an arabic presentation form b character (commenly given by Windows when converting from glyphs) #Decompose it to its original basic arabic (non-presentational_ character. if 0xfe70<=ord(c)<=0xfeff: d=unicodedata.decomposition(c) d=d.split(' ') if d else None if d and len(d)==2 and d[0] in ('','','',''): c=unichr(int(d[1],16)) l.append(c) return u"".join(l) def yieldListRange(l,start,stop): for x in xrange(start,stop): yield l[x] def processWindowChunksInLine(commandList,rects,startIndex,startOffset,endIndex,endOffset): windowStartIndex=startIndex lastEndOffset=windowStartOffset=startOffset lastHwnd=None for index in xrange(startIndex,endIndex+1): item=commandList[index] if index0 else None:-1] rectsStart=runStartOffset for i in xrange(runStartIndex,index,2): command=commandList[i] text=commandList[i+1] rectsEnd=rectsStart+len(text) commandList[i+1]=command shouldReverseText=command.field.get('shouldReverseText',True) commandList[i]=normalizeRtlString(text[::-1] if shouldReverseText else text) if not shouldReverseText: #Because all the rects in the run were already reversed, we need to undo that for this field rects[rectsStart:rectsEnd]=rects[rectsEnd-1:rectsStart-1 if rectsStart>0 else None:-1] rectsStart=rectsEnd #Reverse commandList commandList[runStartIndex:index]=commandList[index-1:runStartIndex-1 if runStartIndex>0 else None:-1] if overallDirection<0: #As the overall reading direction of the passage is rtl, record the location of this run so we can reverse the order of runs later reorderList.append((runStartIndex,runStartOffset,index,lastEndOffset)) if item: runStartIndex=index runStartOffset=lastEndOffset runDirection=direction if overallDirection<0: # As the overall reading direction of the passage is rtl, build a new command list and rects list with the order of runs reversed # The content of each run is already in logical reading order itself newCommandList=[] newRects=[] for si,so,ei,eo in reversed(reorderList): newCommandList.extend(yieldListRange(commandList,si,ei)) newRects.extend(yieldListRange(rects,so,eo)) # Update the original command list and rect list replacing the old content for this passage with the reordered runs commandList[startIndex:endIndex]=newCommandList rects[startOffset:endOffset]=newRects _getWindowTextInRect=None _requestTextChangeNotificationsForWindow=None #: Objects that have registered for text change notifications. _textChangeNotificationObjs=[] def initialize(): global _getWindowTextInRect,_requestTextChangeNotificationsForWindow, _getFocusRect _getWindowTextInRect=CFUNCTYPE(c_long,c_long,c_long,c_bool,c_int,c_int,c_int,c_int,c_int,c_int,c_bool,POINTER(BSTR),POINTER(BSTR))(('displayModel_getWindowTextInRect',NVDAHelper.localLib),((1,),(1,),(1,),(1,),(1,),(1,),(1,),(1,),(1,),(1,),(2,),(2,))) _requestTextChangeNotificationsForWindow=NVDAHelper.localLib.displayModel_requestTextChangeNotificationsForWindow def getCaretRect(obj): left=c_long() top=c_long() right=c_long() bottom=c_long() res=watchdog.cancellableExecute(NVDAHelper.localLib.displayModel_getCaretRect, obj.appModule.helperLocalBindingHandle, obj.windowThreadID, byref(left),byref(top),byref(right),byref(bottom)) if res!=0: raise RuntimeError("displayModel_getCaretRect failed with res %d"%res) return RECT(left,top,right,bottom) def getWindowTextInRect(bindingHandle, windowHandle, left, top, right, bottom,minHorizontalWhitespace,minVerticalWhitespace,stripOuterWhitespace=True,includeDescendantWindows=True): text, cpBuf = watchdog.cancellableExecute(_getWindowTextInRect, bindingHandle, windowHandle, includeDescendantWindows, left, top, right, bottom,minHorizontalWhitespace,minVerticalWhitespace,stripOuterWhitespace) if not text or not cpBuf: return u"",[] characterLocations = [] cpBufIt = iter(cpBuf) for cp in cpBufIt: characterLocations.append((ord(cp), ord(next(cpBufIt)), ord(next(cpBufIt)), ord(next(cpBufIt)))) return text, characterLocations def getFocusRect(obj): left=c_long() top=c_long() right=c_long() bottom=c_long() if NVDAHelper.localLib.displayModel_getFocusRect(obj.appModule.helperLocalBindingHandle,obj.windowHandle,byref(left),byref(top),byref(right),byref(bottom))==0: return left.value,top.value,right.value,bottom.value return None def requestTextChangeNotifications(obj, enable): """Request or cancel notifications for when the display text changes in an NVDAObject. A textChange event (event_textChange) will be fired on the object when its text changes. Note that this event does not provide any information about the changed text itself. It is important to request that notifications be cancelled when you no longer require them or when the object is no longer in use, as otherwise, resources will not be released. @param obj: The NVDAObject for which text change notifications are desired. @type obj: NVDAObject @param enable: C{True} to enable notifications, C{False} to disable them. @type enable: bool """ if not enable: _textChangeNotificationObjs.remove(obj) watchdog.cancellableExecute(_requestTextChangeNotificationsForWindow, obj.appModule.helperLocalBindingHandle, obj.windowHandle, enable) if enable: _textChangeNotificationObjs.append(obj) def textChangeNotify(windowHandle, left, top, right, bottom): for obj in _textChangeNotificationObjs: if windowHandle == obj.windowHandle: # It is safe to call this event from this RPC thread. # This avoids an extra core cycle. obj.event_textChange() class DisplayModelTextInfo(OffsetsTextInfo): minHorizontalWhitespace=8 minVerticalWhitespace=32 stripOuterWhitespace=True includeDescendantWindows=True def _get_backgroundSelectionColor(self): self.backgroundSelectionColor=colors.RGB.fromCOLORREF(winUser.user32.GetSysColor(13)) return self.backgroundSelectionColor def _get_foregroundSelectionColor(self): self.foregroundSelectionColor=colors.RGB.fromCOLORREF(winUser.user32.GetSysColor(14)) return self.foregroundSelectionColor def _getSelectionOffsets(self): if self.backgroundSelectionColor is not None and self.foregroundSelectionColor is not None: fields=self._storyFieldsAndRects[0] startOffset=None endOffset=None curOffset=0 inHighlightChunk=False for item in fields: if isinstance(item,textInfos.FieldCommand) and item.command=="formatChange" and item.field.get('color',None)==self.foregroundSelectionColor and item.field.get('background-color',None)==self.backgroundSelectionColor: inHighlightChunk=True if startOffset is None: startOffset=curOffset elif isinstance(item,basestring): curOffset+=len(item) if inHighlightChunk: endOffset=curOffset else: inHighlightChunk=False if startOffset is not None and endOffset is not None: return (startOffset,endOffset) raise LookupError def __init__(self, obj, position,limitRect=None): if isinstance(position, textInfos.Rect): limitRect=position position=textInfos.POSITION_ALL if limitRect is not None: self._location = limitRect.left, limitRect.top, limitRect.right, limitRect.bottom else: self._location = None super(DisplayModelTextInfo, self).__init__(obj, position) _cache__storyFieldsAndRects = True def _get__storyFieldsAndRects(self): # All returned coordinates are logical coordinates. if self._location: left, top, right, bottom = self._location else: try: left, top, width, height = self.obj.location except TypeError: # No location; nothing we can do. return [],[],[] right = left + width bottom = top + height bindingHandle=self.obj.appModule.helperLocalBindingHandle if not bindingHandle: log.debugWarning("AppModule does not have a binding handle") return [],[],[] left,top=windowUtils.physicalToLogicalPoint(self.obj.windowHandle,left,top) right,bottom=windowUtils.physicalToLogicalPoint(self.obj.windowHandle,right,bottom) text,rects=getWindowTextInRect(bindingHandle, self.obj.windowHandle, left, top, right, bottom, self.minHorizontalWhitespace, self.minVerticalWhitespace,self.stripOuterWhitespace,self.includeDescendantWindows) if not text: return [],[],[] text="%s"%text commandList=XMLFormatting.XMLTextParser().parse(text) curFormatField=None lastEndOffset=0 lineStartOffset=0 lineStartIndex=0 lineBaseline=None lineEndOffsets=[] for index in xrange(len(commandList)): item=commandList[index] if isinstance(item,basestring): lastEndOffset+=len(item) elif isinstance(item,textInfos.FieldCommand): if isinstance(item.field,textInfos.FormatField): curFormatField=item.field self._normalizeFormatField(curFormatField) else: curFormatField=None baseline=curFormatField['baseline'] if curFormatField else None if baseline!=lineBaseline: if lineBaseline is not None: processWindowChunksInLine(commandList,rects,lineStartIndex,lineStartOffset,index,lastEndOffset) #Convert the whitespace at the end of the line into a line feed item=commandList[index-1] if isinstance(item,basestring) and len(item)==1 and item.isspace(): commandList[index-1]=u'\n' lineEndOffsets.append(lastEndOffset) if baseline is not None: lineStartIndex=index lineStartOffset=lastEndOffset lineBaseline=baseline return commandList,rects,lineEndOffsets def _getStoryOffsetLocations(self): baseline=None direction=0 lastEndOffset=0 commandList,rects,lineEndOffsets=self._storyFieldsAndRects for item in commandList: if isinstance(item,textInfos.FieldCommand) and isinstance(item.field,textInfos.FormatField): baseline=item.field['baseline'] direction=item.field['direction'] elif isinstance(item,basestring): endOffset=lastEndOffset+len(item) for rect in rects[lastEndOffset:endOffset]: yield rect,baseline,direction lastEndOffset=endOffset def _getFieldsInRange(self,start,end): storyFields=self._storyFieldsAndRects[0] if not storyFields: return [] #Strip unwanted commands and text from the start and the end to honour the requested offsets lastEndOffset=0 startIndex=endIndex=relStart=relEnd=None for index in xrange(len(storyFields)): item=storyFields[index] if isinstance(item,basestring): endOffset=lastEndOffset+len(item) if lastEndOffset<=start=len(rects): raise LookupError x,y=rects[offset][:2] x,y=windowUtils.logicalToPhysicalPoint(self.obj.windowHandle,x,y) return textInfos.Point(x, y) def _getOffsetFromPoint(self, x, y): # Accepts physical coordinates. x,y=windowUtils.physicalToLogicalPoint(self.obj.windowHandle,x,y) for charOffset, (charLeft, charTop, charRight, charBottom) in enumerate(self._storyFieldsAndRects[1]): if charLeft<=x0 else 0 def _getNVDAObjectFromOffset(self,offset): try: p=self._getPointFromOffset(offset) except (NotImplementedError,LookupError): return self.obj obj=api.getDesktopObject().objectFromPoint(p.x,p.y) from NVDAObjects.window import Window if not obj or not isinstance(obj,Window) or not winUser.isDescendantWindow(self.obj.windowHandle,obj.windowHandle): return self.obj return obj def _getOffsetsFromNVDAObject(self,obj): l=obj.location if not l: log.debugWarning("object has no location") raise LookupError x=l[0]+(l[2]/2) y=l[1]+(l[3]/2) offset=self._getClosestOffsetFromPoint(x,y) return offset,offset def _getLineOffsets(self,offset): lineEndOffsets=self._storyFieldsAndRects[2] if not lineEndOffsets: return offset,offset+1 limit=lineEndOffsets[-1] if not limit: return offset,offset+1 offset=min(offset,limit-1) startOffset=0 endOffset=0 for lineEndOffset in lineEndOffsets: startOffset=endOffset endOffset=lineEndOffset if lineEndOffset>offset: break return startOffset,endOffset def _get_clipboardText(self): return "\r\n".join(x.strip('\r\n') for x in self.getTextInChunks(textInfos.UNIT_LINE)) def getTextInChunks(self,unit): #Specifically handle the line unit as we have the line offsets pre-calculated, and we can not guarantee lines end with \n if unit is textInfos.UNIT_LINE: text=self.text relStart=0 for lineEndOffset in self._storyFieldsAndRects[2]: if lineEndOffset<=self._startOffset: continue relEnd=min(self._endOffset,lineEndOffset)-self._startOffset yield text[relStart:relEnd] relStart=relEnd if lineEndOffset>=self._endOffset: return return for chunk in super(DisplayModelTextInfo,self)._getTextInChunks(unit): yield chunk class EditableTextDisplayModelTextInfo(DisplayModelTextInfo): minHorizontalWhitespace=1 minVerticalWhitespace=4 stripOuterWhitespace=False def _findCaretOffsetFromLocation(self,caretRect,validateBaseline=True,validateDirection=True): # Accepts logical coordinates. for charOffset, ((charLeft, charTop, charRight, charBottom),charBaseline,charDirection) in enumerate(self._getStoryOffsetLocations()): # Skip any character that does not overlap the caret vertically if (caretRect.bottom<=charTop or caretRect.top>=charBottom): continue # Skip any character that does not overlap the caret horizontally if (caretRect.right<=charLeft or caretRect.left>=charRight): continue # skip over any character that does not have a baseline or who's baseline the caret does not go through if validateBaseline and (charBaseline<0 or not (caretRect.top0) or (not charDirection<0 and direction<0): continue return charOffset raise LookupError def _getCaretOffset(self): caretRect=getCaretRect(self.obj) objLocation=self.obj.location objRect=RECT(objLocation[0],objLocation[1],objLocation[0]+objLocation[2],objLocation[1]+objLocation[3]) objRect.left,objRect.top=windowUtils.physicalToLogicalPoint( self.obj.windowHandle,objRect.left,objRect.top) objRect.right,objRect.bottom=windowUtils.physicalToLogicalPoint( self.obj.windowHandle,objRect.right,objRect.bottom) caretRect.left=max(objRect.left,caretRect.left) caretRect.top=max(objRect.top,caretRect.top) caretRect.right=min(objRect.right,caretRect.right) caretRect.bottom=min(objRect.bottom,caretRect.bottom) # Find a character offset where the caret overlaps vertically, overlaps horizontally, overlaps the baseline and is totally within or on the correct side for the reading order try: return self._findCaretOffsetFromLocation(caretRect,validateBaseline=True,validateDirection=True) except LookupError: pass # Find a character offset where the caret overlaps vertically, overlaps horizontally, overlaps the baseline, but does not care about reading order (probably whitespace at beginning or end of a line) try: return self._findCaretOffsetFromLocation(caretRect,validateBaseline=True,validateDirection=False) except LookupError: pass # Find a character offset where the caret overlaps vertically, overlaps horizontally, but does not care about baseline or reading order (probably vertical whitespace -- blank lines) try: return self._findCaretOffsetFromLocation(caretRect,validateBaseline=False,validateDirection=False) except LookupError: raise RuntimeError def _setCaretOffset(self,offset): rects=self._storyFieldsAndRects[1] if offset>=len(rects): raise RuntimeError("offset %d out of range") left,top,right,bottom=rects[offset] x=left #+(right-left)/2 y=top+(bottom-top)/2 x,y=windowUtils.logicalToPhysicalPoint(self.obj.windowHandle,x,y) oldX,oldY=winUser.getCursorPos() winUser.setCursorPos(x,y) winUser.mouse_event(winUser.MOUSEEVENTF_LEFTDOWN,0,0,None,None) winUser.mouse_event(winUser.MOUSEEVENTF_LEFTUP,0,0,None,None) winUser.setCursorPos(oldX,oldY) def _getSelectionOffsets(self): try: return super(EditableTextDisplayModelTextInfo,self)._getSelectionOffsets() except LookupError: offset=self._getCaretOffset() return offset,offset def _setSelectionOffsets(self,start,end): if start!=end: raise NotImplementedError("Expanded selections not supported") self._setCaretOffset(start)