#appModules/powerpnt.py #A part of NonVisual Desktop Access (NVDA) #Copyright (C) 2012-2015 NV Access Limited #This file is covered by the GNU General Public License. #See the file COPYING for more details. import comtypes from comtypes.automation import IDispatch import comtypes.client import ctypes import oleacc import comHelper import ui import queueHandler import colors import api import speech import sayAllHandler import NVDAHelper import winUser from treeInterceptorHandler import DocumentTreeInterceptor from NVDAObjects import NVDAObjectTextInfo from displayModel import DisplayModelTextInfo, EditableTextDisplayModelTextInfo import textInfos.offsets import eventHandler import appModuleHandler from NVDAObjects.IAccessible import IAccessible, getNVDAObjectFromEvent from NVDAObjects.window import Window from NVDAObjects.behaviors import EditableTextWithoutAutoSelectDetection, EditableText import braille from cursorManager import ReviewCursorManager import controlTypes from logHandler import log import scriptHandler # Window classes where PowerPoint's object model should be used # These also all request to have their (incomplete) UI Automation implementations disabled. [MS Office 2013] objectModelWindowClasses=set(["paneClassDC","mdiClass","screenClass"]) MATHTYPE_PROGID = "Equation.DSMT" #comtypes COM interface definition for Powerpoint application object's events class EApplication(IDispatch): _iid_=comtypes.GUID('{914934C2-5A91-11CF-8700-00AA0060263B}') _methods_=[] _disp_methods_=[ comtypes.DISPMETHOD([comtypes.dispid(2001)],None,"WindowSelectionChange", (['in'],ctypes.POINTER(IDispatch),'sel'),), comtypes.DISPMETHOD([comtypes.dispid(2013)],None,"SlideShowNextSlide", (['in'],ctypes.POINTER(IDispatch),'slideShowWindow'),), ] #Our implementation of the EApplication COM interface to receive application events class ppEApplicationSink(comtypes.COMObject): _com_interfaces_=[EApplication,IDispatch] def SlideShowNextSlide(self,slideShowWindow=None): i=winUser.getGUIThreadInfo(0) oldFocus=api.getFocusObject() if not isinstance(oldFocus,SlideShowWindow) or i.hwndFocus!=oldFocus.windowHandle: return oldFocus.treeInterceptor.rootNVDAObject.handleSlideChange() def WindowSelectionChange(self,sel): i=winUser.getGUIThreadInfo(0) oldFocus=api.getFocusObject() if not isinstance(oldFocus,Window) or i.hwndFocus!=oldFocus.windowHandle: return if isinstance(oldFocus,DocumentWindow): documentWindow=oldFocus elif isinstance(oldFocus,PpObject): documentWindow=oldFocus.documentWindow else: return documentWindow.ppSelection=sel documentWindow.handleSelectionChange() #Bullet types ppBulletNumbered=2 # values for enumeration 'PpPlaceholderType' ppPlaceholderMixed = -2 ppPlaceholderTitle = 1 ppPlaceholderBody = 2 ppPlaceholderCenterTitle = 3 ppPlaceholderSubtitle = 4 ppPlaceholderVerticalTitle = 5 ppPlaceholderVerticalBody = 6 ppPlaceholderObject = 7 ppPlaceholderChart = 8 ppPlaceholderBitmap = 9 ppPlaceholderMediaClip = 10 ppPlaceholderOrgChart = 11 ppPlaceholderTable = 12 ppPlaceholderSlideNumber = 13 ppPlaceholderHeader = 14 ppPlaceholderFooter = 15 ppPlaceholderDate = 16 ppPlaceholderVerticalObject = 17 ppPlaceholderPicture = 18 ppPlaceholderLabels={ # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderTitle:_("Title placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderBody:_("Text placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderCenterTitle:_("Center Title placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderSubtitle:_("Subtitle placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderVerticalTitle:_("Vertical Title placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderVerticalBody:_("Vertical Text placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderObject:_("Object placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderChart:_("Chart placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderBitmap:_("Bitmap placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderMediaClip:_("Media Clip placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderOrgChart:_("Org Chart placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderTable:_("Table placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderSlideNumber:_("Slide Number placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderHeader:_("Header placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderFooter:_("Footer placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderDate:_("Date placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderVerticalObject:_("Vertical Object placeholder"), # Translators: Describes a type of placeholder shape in Microsoft PowerPoint. ppPlaceholderPicture:_("Picture placeholder"), } #selection types ppSelectionNone=0 ppSelectionSlides=1 ppSelectionShapes=2 ppSelectionText=3 # values for enumeration 'PpViewType' ppViewSlide = 1 ppViewSlideMaster = 2 ppViewNotesPage = 3 ppViewHandoutMaster = 4 ppViewNotesMaster = 5 ppViewOutline = 6 ppViewSlideSorter = 7 ppViewTitleMaster = 8 ppViewNormal = 9 ppViewPrintPreview = 10 ppViewThumbnails = 11 ppViewMasterThumbnails = 12 ppViewTypeLabels={ # Translators: a label for a particular view or pane in Microsoft PowerPoint ppViewSlide:_("Slide view"), # Translators: a label for a particular view or pane in Microsoft PowerPoint ppViewSlideMaster:_("Slide Master view"), # Translators: a label for a particular view or pane in Microsoft PowerPoint ppViewNotesPage:_("Notes page"), # Translators: a label for a particular view or pane in Microsoft PowerPoint ppViewHandoutMaster:_("Handout Master view"), # Translators: a label for a particular view or pane in Microsoft PowerPoint ppViewNotesMaster:_("Notes Master view"), # Translators: a label for a particular view or pane in Microsoft PowerPoint ppViewOutline:_("Outline view"), # Translators: a label for a particular view or pane in Microsoft PowerPoint ppViewSlideSorter:_("Slide Sorter view"), # Translators: a label for a particular view or pane in Microsoft PowerPoint ppViewTitleMaster:_("Title Master view"), # Translators: a label for a particular view or pane in Microsoft PowerPoint ppViewNormal:_("Normal view"), # Translators: a label for a particular view or pane in Microsoft PowerPoint ppViewPrintPreview:_("Print Preview"), # Translators: a label for a particular view or pane in Microsoft PowerPoint ppViewThumbnails:_("Thumbnails"), # Translators: a label for a particular view or pane in Microsoft PowerPoint ppViewMasterThumbnails:_("Master Thumbnails"), } # values for enumeration 'MsoShapeType' msoShapeTypeMixed = -2 msoAutoShape = 1 msoCallout = 2 msoChart = 3 msoComment = 4 msoFreeform = 5 msoGroup = 6 msoEmbeddedOLEObject = 7 msoFormControl = 8 msoLine = 9 msoLinkedOLEObject = 10 msoLinkedPicture = 11 msoOLEControlObject = 12 msoPicture = 13 msoPlaceholder = 14 msoTextEffect = 15 msoMedia = 16 msoTextBox = 17 msoScriptAnchor = 18 msoTable = 19 msoCanvas = 20 msoDiagram = 21 msoInk = 22 msoInkComment = 23 msoSmartArt = 24 msoShapeTypesToNVDARoles={ msoChart:controlTypes.ROLE_CHART, msoGroup:controlTypes.ROLE_GROUPING, msoEmbeddedOLEObject:controlTypes.ROLE_EMBEDDEDOBJECT, msoLine:controlTypes.ROLE_LINE, msoLinkedOLEObject:controlTypes.ROLE_EMBEDDEDOBJECT, msoLinkedPicture:controlTypes.ROLE_GRAPHIC, msoPicture:controlTypes.ROLE_GRAPHIC, msoTextBox:controlTypes.ROLE_TEXTFRAME, msoTable:controlTypes.ROLE_TABLE, msoCanvas:controlTypes.ROLE_CANVAS, msoDiagram:controlTypes.ROLE_DIAGRAM, } # PpMouseActivation ppMouseClick=1 # PpActionType ppActionHyperlink=7 def getBulletText(ppBulletFormat): t=ppBulletFormat.type if t==ppBulletNumbered: return "%d."%ppBulletFormat.number #(ppBulletFormat.startValue+(ppBulletFormat.number-1)) elif t: return unichr(ppBulletFormat.character) def walkPpShapeRange(ppShapeRange): for ppShape in ppShapeRange: if ppShape.type==msoGroup: for ppChildShape in walkPpShapeRange(ppShape.groupItems): yield ppChildShape else: yield ppShape class PaneClassDC(Window): """Handles fetching of the Powerpoint object model.""" presentationType=Window.presType_content role=controlTypes.ROLE_PANE value=None TextInfo=DisplayModelTextInfo _cache_currentSlide=False def _get_currentSlide(self): try: ppSlide=self.ppObjectModel.view.slide except comtypes.COMError: return None self.currentSlide=SlideBase(windowHandle=self.windowHandle,documentWindow=self,ppObject=ppSlide) return self.currentSlide def _get_ppVersionMajor(self): self.ppVersionMajor=int(self.ppObjectModel.application.version.split('.')[0]) return self.ppVersionMajor class DocumentWindow(PaneClassDC): """Represents the document window for a presentation. Bounces focus to the currently selected slide, shape or text frame.""" def _get_ppDocumentViewType(self): try: viewType=self.ppObjectModel.viewType except comtypes.COMError: return None self.ppDocumentViewType=viewType return self.ppDocumentViewType def _get_ppActivePaneViewType(self): try: viewType=self.ppObjectModel.activePane.viewType except comtypes.COMError: return None self.ppActivePaneViewType=viewType return self.ppActivePaneViewType def _isEqual(self,other): return self.windowHandle==other.windowHandle and self.name==other.name def _get_name(self): label=ppViewTypeLabels.get(self.ppActivePaneViewType) if not label: return super(PaneClassDC,self).name slide=self.currentSlide if slide: label=" - ".join([slide.name,label]) return label def _get_currentSlide(self): if self.ppActivePaneViewType in (ppViewSlideSorter,ppViewThumbnails,ppViewMasterThumbnails): return None return super(DocumentWindow,self).currentSlide def _get_ppSelection(self): """Fetches and caches the current Powerpoint Selection object for the current presentation.""" self.ppSelection=self.ppObjectModel.selection return self.ppSelection def _get_selection(self): """Fetches an NVDAObject representing the current presentation's selected slide, shape or text frame.""" sel=self.ppSelection selType=sel.type #MS Powerpoint 2007 and below does not correctly indecate text selection in the notes page when in normal view if selType==0 and self.ppVersionMajor<=12: if self.ppActivePaneViewType==ppViewNotesPage and self.ppDocumentViewType==ppViewNormal: selType=ppSelectionText if selType==ppSelectionShapes: #Shape #The selected shape could be within a group shape if sel.hasChildShapeRange: ppObj=sel.childShapeRange[1] else: #a normal top level shape ppObj=sel.shapeRange[1] #Specifically handle shapes representing a table as they have row and column counts etc if ppObj.hasTable: return Table(windowHandle=self.windowHandle,documentWindow=self,ppObject=ppObj) else: #Generic shape return Shape(windowHandle=self.windowHandle,documentWindow=self,ppObject=ppObj) elif selType==ppSelectionText: #Text frame #TextRange objects in Powerpoint do not allow moving/expanding. #Therefore A full TextRange object must be fetched from the original TextFrame the selection is in. #MS Powerpoint 2003 also throws COMErrors when there is no TextRange.parent try: ppObj=sel.textRange.parent except comtypes.COMError: ppObj=None if ppObj: return TextFrame(windowHandle=self.windowHandle,documentWindow=self,ppObject=ppObj) #For TextRange objects for TextFrame objects in table cells and the notes page, TextRange object's parent does not work! #For tables: Get the shape the selection is in -- should be the table. try: shape=sel.shapeRange[1] except comtypes.COMError: shape=None if shape and shape.hasTable: #Only way to find the selected cell in a table is to... walk through it. selectedTableCell=None colNum=0 for col in shape.table.columns: colNum+=1 rowNum=0 for tableCell in col.cells: rowNum+=1 if tableCell.selected: selectedTableCell=tableCell break if selectedTableCell: break if selectedTableCell: #We found the selected table cell #The TextFrame we want is within the shape in this table cell. #However, the shape in the table cell seems to always be rather broken -- hardly any properties work. #Therefore, Explicitly set the table cell as the TextFrame object's parent, skipping the broken shape ppObj=selectedTableCell.shape.textFrame obj=TableCellTextFrame(windowHandle=self.windowHandle,documentWindow=self,ppObject=ppObj) table=Table(windowHandle=self.windowHandle,documentWindow=self,ppObject=shape) obj.parent=TableCell(windowHandle=self.windowHandle,documentWindow=self,ppObject=selectedTableCell,table=table,rowNumber=rowNum,columnNumber=colNum) return obj #TextRange object did not have a parent, and we're not in a table. if self.ppActivePaneViewType==ppViewNotesPage and self.ppDocumentViewType==ppViewNormal: #We're in the notes page in normal view #The TextFrame in this case will be located in the notes page's second shape. slide=sel.slideRange[1] notesPage=slide.notesPage[1] shape=notesPage.shapes[2] ppObj=shape.textFrame return NotesTextFrame(windowHandle=self.windowHandle,documentWindow=self,ppObject=ppObj) if ppObj: return TextFrame(windowHandle=self.windowHandle,documentWindow=self,ppObject=ppObj) if selType==ppSelectionSlides: try: ppObj=sel.slideRange[1] except comtypes.COMError: #Master thumbnails sets the selected slide as view.slide but not selection.slideRange try: ppObj=self.ppObjectModel.view.slide except comtypes.COMError: ppObj=None if ppObj: return SlideBase(windowHandle=self.windowHandle,documentWindow=self,ppObject=ppObj) def _get_focusRedirect(self): return self.selection def _get_firstChild(self): return self.selection def handleSelectionChange(self): """Pushes focus to the newly selected object.""" if getattr(self,"_isHandlingSelectionChange",False): # #3394: A COM event can cause this function to run within itself. # This can cause double speaking, so stop here if we're already running. return self._isHandlingSelectionChange=True try: obj=self.selection if not obj: obj=IAccessible(windowHandle=self.windowHandle,IAccessibleObject=self.IAccessibleObject,IAccessibleChildID=self.IAccessibleChildID) if obj and obj!=eventHandler.lastQueuedFocusObject: eventHandler.queueEvent("gainFocus",obj) finally: self._isHandlingSelectionChange=False def event_gainFocus(self): """Bounces focus to the currently selected slide, shape or Text frame.""" obj=self.selection if obj: eventHandler.queueEvent("focusEntered",self) eventHandler.queueEvent("gainFocus",obj) else: super(DocumentWindow,self).event_gainFocus() def script_selectionChange(self,gesture): gesture.send() if scriptHandler.isScriptWaiting(): return self.handleSelectionChange() script_selectionChange.canPropagate=True __gestures={k:"selectionChange" for k in ( "kb:tab","kb:shift+tab", "kb:leftArrow","kb:rightArrow","kb:upArrow","kb:downArrow", "kb:shift+leftArrow","kb:shift+rightArrow","kb:shift+upArrow","kb:shift+downArrow", "kb:pageUp","kb:pageDown", "kb:home","kb:control+home","kb:end","kb:control+end", "kb:shift+home","kb:shift+control+home","kb:shift+end","kb:shift+control+end", "kb:delete","kb:backspace", )} class OutlinePane(EditableTextWithoutAutoSelectDetection,PaneClassDC): TextInfo=EditableTextDisplayModelTextInfo role=controlTypes.ROLE_EDITABLETEXT class PpObject(Window): """ The base NVDAObject for slides, shapes and text frames. Accepts and holds references to the original Document window, and the current object's Powerpoint object. Also has some utility functions and scripts for managing selection changes. Note No events are used to detect selection changes, its all keyboard commands for now. """ next=None previous=None def __init__(self,windowHandle=None,documentWindow=None,ppObject=None): self.documentWindow=documentWindow self.ppObject=ppObject super(PpObject,self).__init__(windowHandle=windowHandle) def _get_parent(self): return self.documentWindow def script_selectionChange(self,gesture): return self.documentWindow.script_selectionChange(gesture) __gestures={ "kb:escape":"selectionChange", } class SlideBase(PpObject): presentationType=Window.presType_content def findOverlayClasses(self,clsList): if isinstance(self.documentWindow,DocumentWindow) and self.documentWindow.ppActivePaneViewType in (ppViewSlideMaster,ppViewTitleMaster,ppViewNotesMaster,ppViewHandoutMaster,ppViewMasterThumbnails): clsList.append(Master) else: clsList.append(Slide) clsList.append(SlideBase) def _isEqual(self,other): return super(SlideBase,self)._isEqual(other) and self.name==other.name role=controlTypes.ROLE_PANE class Slide(SlideBase): """Represents a single slide in Powerpoint.""" def _get_name(self): try: title=self.ppObject.shapes.title.textFrame.textRange.text except comtypes.COMError: title=None try: number=self.ppObject.slideNumber except comtypes.COMError: number="" # Translators: the label for a slide in Microsoft PowerPoint. name=_("Slide {slideNumber}").format(slideNumber=number) if title: name+=" (%s)"%title return name def _get_positionInfo(self): slideNumber=self.ppObject.slideNumber numSlides=self.ppObject.parent.slides.count return {'indexInGroup':slideNumber,'similarItemsInGroup':numSlides} class Master(SlideBase): def _get_name(self): return self.ppObject.name class Shape(PpObject): """Represents a single shape (rectangle, group, picture, Text bos etc in Powerpoint.""" presentationType=Window.presType_content def __init__(self, **kwargs): super(Shape, self).__init__(**kwargs) if self.role == controlTypes.ROLE_EMBEDDEDOBJECT: if self.ppObject.OLEFormat.ProgID.startswith(MATHTYPE_PROGID): self.role = controlTypes.ROLE_MATH def _get__overlapInfo(self): slideWidth=self.appModule._ppApplication.activePresentation.pageSetup.slideWidth slideHeight=self.appModule._ppApplication.activePresentation.pageSetup.slideHeight left=self.ppObject.left top=self.ppObject.top right=left+self.ppObject.width bottom=top+self.ppObject.height name=self.ppObject.name slideShapeRange=self.documentWindow.currentSlide.ppObject.shapes.range() otherIsBehind=True infrontInfo=None behindInfo=None for ppShape in walkPpShapeRange(slideShapeRange): otherName=ppShape.name if otherName==name: otherIsBehind=False continue otherLeft=ppShape.left otherTop=ppShape.top otherRight=otherLeft+ppShape.width otherBottom=otherTop+ppShape.height if otherLeft>=right or otherRight<=left: continue if otherTop>=bottom or otherBottom<=top: continue info={} info['label']=Shape(windowHandle=self.windowHandle,documentWindow=self.documentWindow,ppObject=ppShape).name info['otherIsBehind']=otherIsBehind info['overlapsOtherLeftBy']=right-otherLeft if right0: total=False if otherIsBehind: # Translators: A message when a shape is infront of another shape on a Powerpoint slide textList.append(_("covers left of {otherShape} by {distance:.3g} points").format(otherShape=otherLabel,distance=overlapsOtherLeftBy)) else: # Translators: A message when a shape is behind another shape on a powerpoint slide textList.append(_("behind left of {otherShape} by {distance:.3g} points").format(otherShape=otherLabel,distance=overlapsOtherLeftBy)) overlapsOtherTopBy=otherInfo['overlapsOtherTopBy'] if overlapsOtherTopBy>0: total=False if otherIsBehind: # Translators: A message when a shape is infront of another shape on a Powerpoint slide textList.append(_("covers top of {otherShape} by {distance:.3g} points").format(otherShape=otherLabel,distance=overlapsOtherTopBy)) else: # Translators: A message when a shape is behind another shape on a powerpoint slide textList.append(_("behind top of {otherShape} by {distance:.3g} points").format(otherShape=otherLabel,distance=overlapsOtherTopBy)) overlapsOtherRightBy=otherInfo['overlapsOtherRightBy'] if overlapsOtherRightBy>0: total=False if otherIsBehind: # Translators: A message when a shape is infront of another shape on a Powerpoint slide textList.append(_("covers right of {otherShape} by {distance:.3g} points").format(otherShape=otherLabel,distance=overlapsOtherRightBy)) else: # Translators: A message when a shape is behind another shape on a powerpoint slide textList.append(_("behind right of {otherShape} by {distance:.3g} points").format(otherShape=otherLabel,distance=overlapsOtherRightBy)) overlapsOtherBottomBy=otherInfo['overlapsOtherBottomBy'] if overlapsOtherBottomBy>0: total=False if otherIsBehind: # Translators: A message when a shape is infront of another shape on a Powerpoint slide textList.append(_("covers bottom of {otherShape} by {distance:.3g} points").format(otherShape=otherLabel,distance=overlapsOtherBottomBy)) else: # Translators: A message when a shape is behind another shape on a powerpoint slide textList.append(_("behind bottom of {otherShape} by {distance:.3g} points").format(otherShape=otherLabel,distance=overlapsOtherBottomBy)) if total: if otherIsBehind: # Translators: A message when a shape is infront of another shape on a Powerpoint slide textList.append(_("covers {otherShape}").format(otherShape=otherLabel)) else: # Translators: A message when a shape is behind another shape on a powerpoint slide textList.append(_("behind {otherShape}").format(otherShape=otherLabel)) return ", ".join(textList) def _get__edgeDistances(self): slideWidth=self.appModule._ppApplication.activePresentation.pageSetup.slideWidth slideHeight=self.appModule._ppApplication.activePresentation.pageSetup.slideHeight leftDistance=self.ppObject.left topDistance=self.ppObject.top rightDistance=slideWidth-(leftDistance+self.ppObject.width) bottomDistance=slideHeight-(topDistance+self.ppObject.height) self._edgeDistances=(leftDistance,topDistance,rightDistance,bottomDistance) return self._edgeDistances def _getShapeLocationText(self,left=False,top=False,right=False,bottom=False): leftDistance,topDistance,rightDistance,bottomDistance=self._edgeDistances offSlideList=[] onSlideList=[] if left: if leftDistance>=0: # Translators: For a shape within a Powerpoint Slide, this is the distance in points from the shape's left edge to the slide's left edge onSlideList.append(_("{distance:.3g} points from left slide edge").format(distance=leftDistance)) else: # Translators: For a shape too far off the left edge of a Powerpoint Slide, this is the distance in points from the shape's left edge (off the slide) to the slide's left edge (where the slide starts) offSlideList.append(_("Off left slide edge by {distance:.3g} points").format(distance=0-leftDistance)) if top: if topDistance>=0: # Translators: For a shape within a Powerpoint Slide, this is the distance in points from the shape's top edge to the slide's top edge onSlideList.append(_("{distance:.3g} points from top slide edge").format(distance=topDistance)) else: # Translators: For a shape too far off the top edge of a Powerpoint Slide, this is the distance in points from the shape's top edge (off the slide) to the slide's top edge (where the slide starts) offSlideList.append(_("Off top slide edge by {distance:.3g} points").format(distance=0-topDistance)) if right: if rightDistance>=0: # Translators: For a shape within a Powerpoint Slide, this is the distance in points from the shape's right edge to the slide's right edge onSlideList.append(_("{distance:.3g} points from right slide edge").format(distance=rightDistance)) else: # Translators: For a shape too far off the right edge of a Powerpoint Slide, this is the distance in points from the shape's right edge (off the slide) to the slide's right edge (where the slide starts) offSlideList.append(_("Off right slide edge by {distance:.3g} points").format(distance=0-rightDistance)) if bottom: if bottomDistance>=0: # Translators: For a shape within a Powerpoint Slide, this is the distance in points from the shape's bottom edge to the slide's bottom edge onSlideList.append(_("{distance:.3g} points from bottom slide edge").format(distance=bottomDistance)) else: # Translators: For a shape too far off the bottom edge of a Powerpoint Slide, this is the distance in points from the shape's bottom edge (off the slide) to the slide's bottom edge (where the slide starts) offSlideList.append(_("Off bottom slide edge by {distance:.3g} points").format(distance=0-bottomDistance)) return ", ".join(offSlideList+onSlideList) def _get_locationText(self): textList=[] text=self._getOverlapText() if text: textList.append(text) text=self._getShapeLocationText(True,True,True,True) if text: textList.append(text) return ", ".join(textList) def _clearLocationCache(self): try: del self._overlapInfo except AttributeError: pass try: del self._edgeDistances except AttributeError: pass def script_moveHorizontal(self,gesture): gesture.send() if scriptHandler.isScriptWaiting(): return self._clearLocationCache() textList=[] text=self._getOverlapText() if text: textList.append(text) text=self._getShapeLocationText(left=True,right=True) if text: textList.append(text) ui.message(", ".join(textList)) def script_moveVertical(self,gesture): gesture.send() if scriptHandler.isScriptWaiting(): return self._clearLocationCache() textList=[] text=self._getOverlapText() if text: textList.append(text) text=self._getShapeLocationText(top=True,bottom=True) if text: textList.append(text) ui.message(", ".join(textList)) def _get_ppPlaceholderType(self): try: return self.ppObject.placeholderFormat.type except comtypes.COMError: return None def _get_location(self): pointLeft=self.ppObject.left pointTop=self.ppObject.top pointWidth=self.ppObject.width pointHeight=self.ppObject.height left=self.documentWindow.ppObjectModel.pointsToScreenPixelsX(pointLeft) 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) def _get_ppShapeType(self): """Fetches and caches the type of this shape.""" self.ppShapeType=self.ppObject.type return self.ppShapeType def _get_name(self): """The name is calculated firstly from the object's title, otherwize if its a generic shape, then part of its programmatic name is used.""" #Powerpoint 2003 shape objects do not have a title property try: title=self.ppObject.title except comtypes.COMError: title=None if title: return title if self.ppShapeType==msoPlaceholder: label=ppPlaceholderLabels.get(self.ppPlaceholderType) if label: return label if self.role==controlTypes.ROLE_SHAPE: name=self.ppObject.name return " ".join(name.split(' ')[:-1]) def _isEqual(self,other): return super(Shape,self)._isEqual(other) and self.ppObject.ID==other.ppObject.ID def _get_description(self): return self.ppObject.alternativeText def _get_role(self): return msoShapeTypesToNVDARoles.get(self.ppShapeType,controlTypes.ROLE_SHAPE) def _get_value(self): if self.ppObject.hasTextFrame: return self.ppObject.textFrame.textRange.text def _get_states(self): states=super(Shape,self).states if self._overlapInfo[1] is not None: states.add(controlTypes.STATE_OBSCURED) if any(x for x in self._edgeDistances if x<0): states.add(controlTypes.STATE_OFFSCREEN) return states def _get_mathMl(self): try: import mathType except: raise LookupError("MathType not installed") obj = self.ppObject.OLEFormat try: # Don't call RunForConversion, as this seems to make focus bounce. # We don't seem to need it for PowerPoint anyway. return mathType.getMathMl(obj, runForConversion=False) except: raise LookupError("Couldn't get MathML from MathType") __gestures={ "kb:leftArrow":"moveHorizontal", "kb:rightArrow":"moveHorizontal", "kb:upArrow":"moveVertical", "kb:downArrow":"moveVertical", "kb:shift+leftArrow":"moveHorizontal", "kb:shift+rightArrow":"moveHorizontal", "kb:shift+upArrow":"moveVertical", "kb:shift+downArrow":"moveVertical", "kb:enter":"selectionChange", "kb:f2":"selectionChange", } class TextFrameTextInfo(textInfos.offsets.OffsetsTextInfo): def _getCaretOffset(self): return self.obj.documentWindow.ppSelection.textRange.start-1 def _getSelectionOffsets(self): sel=self.obj.documentWindow.ppSelection.textRange start=sel.start-1 end=start+sel.length return start,end def _getTextRange(self,start,end): # #4619: First let's "normalise" the text, i.e. get rid of the CR/LF mess text=self.obj.ppObject.textRange.text text=text.replace('\r\n','\n') #Now string slicing will be okay text=text[start:end].replace('\x0b','\n') text=text.replace('\r','\n') return text def _getStoryLength(self): return self.obj.ppObject.textRange.length def _getLineOffsets(self,offset): #Seems to be no direct way to find the line offsets for a given offset. #Therefore walk through all the lines until one surrounds the offset. lines=self.obj.ppObject.textRange.lines() length=lines.length # #3403: handle case where offset is at end of the text in in a control with only one line # The offset should be limited to the last offset in the text, but only if the text does not end in a line feed. if length and offset>=length and self._getTextRange(length-1,length)!='\n': offset=min(offset,length-1) for line in lines: start=line.start-1 end=start+line.length if start<=offset= self._endOffset: break # Output any text before the field. chunk = text[textOffset:fieldOffset] if chunk: out.append(chunk) # Output the field. out.extend(( # Copy the field so the original isn't modified. textInfos.FieldCommand("controlStart", textInfos.ControlField(field)), u" ", textInfos.FieldCommand("controlEnd", None))) textOffset = fieldOffset + 1 # Output any text after all fields in this range. chunk = text[textOffset:self._endOffset] if chunk: out.append(chunk) return out def getMathMl(self, field): try: import mathType except: raise LookupError("MathType not installed") try: # Don't call RunForConversion, as this seems to make focus bounce. # We don't seem to need it for PowerPoint anyway. return mathType.getMathMl(field["oleFormat"], runForConversion=False) except: raise LookupError("Couldn't get MathML from MathType") class SlideShowTreeInterceptor(DocumentTreeInterceptor): """A TreeInterceptor for showing Slide show content. Has no caret navigation, a CursorManager must be used on top. """ def _get_isAlive(self): return winUser.isWindow(self.rootNVDAObject.windowHandle) def __contains__(self,obj): return isinstance(obj,Window) and obj.windowHandle==self.rootNVDAObject.windowHandle hadFocusOnce=False def event_treeInterceptor_gainFocus(self): braille.handler.handleGainFocus(self) self.rootNVDAObject.reportFocus() if not self.hadFocusOnce: self.hadFocusOnce=True self.reportNewSlide() else: info = self.selection if not info.isCollapsed: speech.speakSelectionMessage(_("selected %s"), info.text) else: info.expand(textInfos.UNIT_LINE) speech.speakTextInfo(info, reason=controlTypes.REASON_CARET, unit=textInfos.UNIT_LINE) def event_gainFocus(self,obj,nextHandler): pass TextInfo=SlideShowTreeInterceptorTextInfo def makeTextInfo(self,position): return self.TextInfo(self,position) def reportNewSlide(self): self.makeTextInfo(textInfos.POSITION_FIRST).updateCaret() sayAllHandler.readText(sayAllHandler.CURSOR_CARET) def script_toggleNotesMode(self,gesture): self.rootNVDAObject.notesMode=not self.rootNVDAObject.notesMode self.rootNVDAObject.handleSlideChange() # Translators: The description for a script script_toggleNotesMode.__doc__=_("Toggles between reporting the speaker notes or the actual slide content. This does not change what is visible on-screen, but only what the user can read with NVDA") def script_slideChange(self,gesture): gesture.send() self.rootNVDAObject.handleSlideChange() class ReviewableSlideshowTreeInterceptor(ReviewCursorManager,SlideShowTreeInterceptor): """A TreeInterceptor for Slide show content but with caret navigation via ReviewCursorManager.""" __gestures={ "kb:space":"slideChange", "kb:enter":"slideChange", "kb:backspace":"slideChange", "kb:pageUp":"slideChange", "kb:pageDown":"slideChange", "kb:control+shift+s":"toggleNotesMode", } class SlideShowWindow(PaneClassDC): _lastSlideChangeID=None treeInterceptorClass=ReviewableSlideshowTreeInterceptor notesMode=False #: If true then speaker notes will be exposed as this object's basicText, rather than the actual slide content. def _get_name(self): if self.currentSlide: if self.notesMode: # Translators: The title of the current slide (with notes) in a running Slide Show in Microsoft PowerPoint. return _("Slide show notes - {slideName}").format(slideName=self.currentSlide.name) else: # Translators: The title of the current slide in a running Slide Show in Microsoft PowerPoint. return _("Slide show - {slideName}").format(slideName=self.currentSlide.name) else: # Translators: The title for a Slide show in Microsoft PowerPoint that has completed. return _("Slide Show - complete") value=None def _getShapeText(self,shape,cellShape=False): if shape.hasTextFrame: for p in shape.textFrame.textRange.paragraphs(): bulletText=getBulletText(p.paragraphFormat.bullet) text=p.text.replace('\x0b','\n') text=text.replace('\r','\n') text=text.rstrip() text=" ".join([t for t in (bulletText,text) if t]) if text and not text.isspace(): yield text return if cellShape: return shapeType=shape.type if shapeType==msoGroup: for childShape in shape.groupItems: for chunk in self._getShapeText(childShape): yield chunk return if shape.hasTable: table=shape.table for row in table.rows: for cell in row.cells: for chunk in self._getShapeText(cell.shape,cellShape=True): yield chunk return if shapeType==msoEmbeddedOLEObject: oleFormat=shape.OLEFormat if oleFormat.ProgID.startswith(MATHTYPE_PROGID): yield textInfos.ControlField(role=controlTypes.ROLE_MATH, oleFormat=oleFormat, _startOfNode=True) return label=shape.alternativeText if not label: try: label=shape.title except comtypes.COMError: pass if label: typeName=" ".join(shape.name.split(' ')[:-1]) if typeName and not typeName.isspace(): yield "%s %s"%(typeName,label) else: yield label def _get_basicText(self): if not self.currentSlide: return self.name chunks=[] ppObject=self.currentSlide.ppObject if self.notesMode: ppObject=ppObject.notesPage # Maintain a list of fields and the offsets at which they occur. # For now, these are only control fields that consume a space. fields=self.basicTextFields=[] # Incrementation of textLen must include line feed added by join. textLen=0 for shape in ppObject.shapes: for chunk in self._getShapeText(shape): if isinstance(chunk,textInfos.ControlField): fields.append((textLen,chunk)) chunks.append(" ") textLen+=2 else: chunks.append(chunk) textLen+=len(chunk)+1 self.basicText="\n".join(chunks) if not self.basicText: if self.notesMode: # Translators: The message for no notes for a slide in a slide show self.basicText=_("No notes") else: # Translators: The message for an empty slide in a slide show self.basicText=_("Empty slide") return self.basicText or _("Empty slide") def handleSlideChange(self): try: del self.__dict__['currentSlide'] except KeyError: pass curSlideChangeID=self.name if curSlideChangeID==self._lastSlideChangeID: return self._lastSlideChangeID=curSlideChangeID try: del self.__dict__['basicText'] except KeyError: pass self.reportFocus() self.treeInterceptor.reportNewSlide() class AppModule(appModuleHandler.AppModule): hasTriedPpAppSwitch=False _ppApplicationWindow=None _ppApplication=None _ppEApplicationConnectionPoint=None def isBadUIAWindow(self,hwnd): # PowerPoint 2013 implements UIA support for its slides etc on an mdiClass window. However its far from complete. # We must disable it in order to fall back to our own code. if winUser.getClassName(hwnd) in objectModelWindowClasses: return True return super(AppModule,self).isBadUIAWindow(hwnd) def _registerCOMWithFocusJuggle(self): import wx import gui # Translators: A title for a dialog shown while Microsoft PowerPoint initializes d=wx.Dialog(None,title=_("Waiting for Powerpoint...")) d.Center(wx.BOTH | wx.CENTER_ON_SCREEN) gui.mainFrame.prePopup() d.Show() self.hasTriedPpAppSwitch=True #Make sure NVDA detects and reports focus on the waiting dialog api.processPendingEvents() comtypes.client.PumpEvents(1) d.Destroy() gui.mainFrame.postPopup() def _getPpObjectModelFromWindow(self,windowHandle): """ Fetches the Powerpoint object model from a given window. """ try: pDispatch=oleacc.AccessibleObjectFromWindow(windowHandle,winUser.OBJID_NATIVEOM,interface=comtypes.automation.IDispatch) return comtypes.client.dynamic.Dispatch(pDispatch) except: log.debugWarning("Could not get MS Powerpoint object model",exc_info=True) return None _ppApplicationFromROT=None def _getPpObjectModelFromROT(self,useRPC=False): if not self._ppApplicationFromROT: try: self._ppApplicationFromROT=comHelper.getActiveObject(u'powerPoint.application',dynamic=True,appModule=self if useRPC else None) except: log.debugWarning("Could not get active object via RPC") return None try: pres=self._ppApplicationFromROT.activePresentation except (comtypes.COMError,NameError,AttributeError): log.debugWarning("No active presentation") return None try: ppSlideShowWindow=pres.slideShowWindow except (comtypes.COMError,NameError,AttributeError): log.debugWarning("Could not get slideShowWindow") ppSlideShowWindow=None isActiveSlideShow=False if ppSlideShowWindow: try: isActiveSlideShow=ppSlideShowWindow.active except comtypes.COMError: log.debugWarning("slideShowWindow.active",exc_info=True) if isActiveSlideShow: return ppSlideShowWindow try: window=pres.windows.item(1) except (comtypes.COMError,NameError,AttributeError): window=None return window def _fetchPpObjectModelHelper(self,windowHandle=None): m=None # Its only safe to get the object model from PowerPoint 2003 to 2010 windows. # In PowerPoint 2013 protected mode it causes security/stability issues if windowHandle and winUser.getClassName(windowHandle)=="paneClassDC": m=self._getPpObjectModelFromWindow(windowHandle) if not m: m=self._getPpObjectModelFromROT(useRPC=True) if not m: m=self._getPpObjectModelFromROT() return m def fetchPpObjectModel(self,windowHandle=None): m=self._fetchPpObjectModelHelper(windowHandle=windowHandle) if not m and not self.hasTriedPpAppSwitch: self._registerCOMWithFocusJuggle() m=self._fetchPpObjectModelHelper(windowHandle=windowHandle) if m: if windowHandle!=self._ppApplicationWindow or not self._ppApplication: self._ppApplicationWindow=windowHandle self._ppApplication=m.application sink=ppEApplicationSink().QueryInterface(comtypes.IUnknown) self._ppEApplicationConnectionPoint=comtypes.client._events._AdviseConnection(self._ppApplication,EApplication,sink) return m def chooseNVDAObjectOverlayClasses(self,obj,clsList): if obj.windowClassName in objectModelWindowClasses and isinstance(obj,IAccessible) and not isinstance(obj,PpObject) and obj.event_objectID==winUser.OBJID_CLIENT and controlTypes.STATE_FOCUSED in obj.states: m=self.fetchPpObjectModel(windowHandle=obj.windowHandle) if not m: log.debugWarning("no object model") return try: ppActivePaneViewType=m.activePane.viewType except comtypes.COMError: ppActivePaneViewType=None if ppActivePaneViewType is None: clsList.insert(0,SlideShowWindow) elif ppActivePaneViewType==ppViewOutline: clsList.insert(0,OutlinePane) else: clsList.insert(0,DocumentWindow) obj.ppActivePaneViewType=ppActivePaneViewType obj.ppObjectModel=m