orca r4272 - in branches/phase2/src/orca: . scripts



Author: wwalker
Date: Wed Oct  1 12:23:23 2008
New Revision: 4272
URL: http://svn.gnome.org/viewvc/orca?rev=4272&view=rev

Log:
More work on deciding what a script really is.


Modified:
   branches/phase2/src/orca/script.py
   branches/phase2/src/orca/scripts/default.py

Modified: branches/phase2/src/orca/script.py
==============================================================================
--- branches/phase2/src/orca/script.py	(original)
+++ branches/phase2/src/orca/script.py	Wed Oct  1 12:23:23 2008
@@ -97,7 +97,7 @@
         #
         self._settings = self._loadSettings(userSettings)
 
-        # The object which has the STATE_FOCUS state.
+        # The object which has the STATE_FOCUSED state.
         #
         self.focus = None
 
@@ -111,6 +111,7 @@
         # caretOffset
         # startOffset
         # endOffset
+        # string
         # row
         # column
         # activeDescendantInfo 

Modified: branches/phase2/src/orca/scripts/default.py
==============================================================================
--- branches/phase2/src/orca/scripts/default.py	(original)
+++ branches/phase2/src/orca/scripts/default.py	Wed Oct  1 12:23:23 2008
@@ -137,6 +137,676 @@
 
         return accessible
 
+    def isTextArea(self, accessible):
+        """Returns True if accessible is a GUI component that is for
+        entering text.
+
+        Arguments:
+        - accessible: an accessible
+        """
+        return accessible and \
+            accessible.getRole() in (pyatspi.ROLE_TEXT,
+                                     pyatspi.ROLE_PARAGRAPH,
+                                     pyatspi.ROLE_TERMINAL)
+
+    def isReadOnlyTextArea(self, accessible):
+        """Returns True if accessible is a text entry area that is
+        read only."""
+        state = accessible.getState()
+        readOnly = self.isTextArea(accessible) \
+                   and state.contains(pyatspi.STATE_FOCUSABLE) \
+                   and not state.contains(pyatspi.STATE_EDITABLE)
+        return readOnly
+
+    def getText(self, accessible, startOffset, endOffset):
+        """Returns the substring of the given accessible's text
+        specialization.
+
+        Arguments:
+        - accessible: an accessible supporting the accessible text 
+        specialization
+        - startOffset: the starting character position
+        - endOffset: the ending character position
+        """
+        return accessible.queryText().getText(startOffset, endOffset)
+
+    def isFunctionalDialog(self, accessible):
+        """Returns true if the window is a functioning as a dialog.
+        This method should be subclassed by application scripts as needed.
+        """
+        return False
+
+    def getUnfocusedAlertAndDialogCount(self, accessible):
+        """If the current application has one or more alert or dialog
+        windows and the currently focused window is not an alert or a dialog,
+        return a count of the number of alert and dialog windows, otherwise
+        return a count of zero.
+
+        Arguments:
+        - accessible: the Accessible object
+
+        Returns the alert and dialog count.
+        """
+        alertAndDialogCount = 0
+        application = accessible.getApplication()
+        window = self.getTopLevel(accessible)
+        if window.getRole() != pyatspi.ROLE_ALERT and \
+           window.getRole() != pyatspi.ROLE_DIALOG and \
+           not self.isFunctionalDialog(window):
+            for child in application:
+                if child.getRole() == pyatspi.ROLE_ALERT or \
+                   child.getRole() == pyatspi.ROLE_DIALOG or \
+                   self.isFunctionalDialog(child):
+                    alertAndDialogCount += 1
+
+        return alertAndDialogCount
+
+    def findCommonAncestor(self, a, b):
+        """Finds the common ancestor between Accessible a and Accessible b.
+
+        Arguments:
+        - a: Accessible
+        - b: Accessible
+        """
+        if (not a) or (not b):
+            return None
+
+        if a == b:
+            return a
+
+        aParents = [a]
+        try:
+            parent = a.parent
+            while parent and (parent.parent != parent):
+                aParents.append(parent)
+                parent = parent.parent
+            aParents.reverse()
+        except:
+            log.exception("exception handled while looking at parent:")
+
+        bParents = [b]
+        try:
+            parent = b.parent
+            while parent and (parent.parent != parent):
+                bParents.append(parent)
+                parent = parent.parent
+            bParents.reverse()
+        except:
+            log.exception("exception handled while looking at parent:")
+
+        commonAncestor = None
+
+        maxSearch = min(len(aParents), len(bParents))
+        i = 0
+        while i < maxSearch:
+            if self.isSameObject(aParents[i], bParents[i]):
+                commonAncestor = aParents[i]
+                i += 1
+            else:
+                break
+
+        return commonAncestor
+
+    def isSameObject(self, obj1, obj2):
+        if (obj1 == obj2):
+            return True
+        elif (not obj1) or (not obj2):
+            return False
+
+        try:
+            if (obj1.name != obj2.name) or (obj1.getRole() != obj2.getRole()):
+                return False
+            else:
+                # Gecko sometimes creates multiple accessibles to represent
+                # the same object.  If the two objects have the same name
+                # and the same role, check the extents.  If those also match
+                # then the two objects are for all intents and purposes the
+                # same object.
+                #
+                extents1 = \
+                    obj1.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
+                extents2 = \
+                    obj2.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
+                if (extents1.x == extents2.x) and \
+                   (extents1.y == extents2.y) and \
+                   (extents1.width == extents2.width) and \
+                   (extents1.height == extents2.height):
+                    return True
+
+            # When we're looking at children of objects that manage
+            # their descendants, we will often get different objects
+            # that point to the same logical child.  We want to be able
+            # to determine if two objects are in fact pointing to the
+            # same child.
+            # If we cannot do so easily (i.e., object equivalence), we examine
+            # the hierarchy and the object index at each level.
+            #
+            parent1 = obj1
+            parent2 = obj2
+            while (parent1 and parent2 and \
+                    parent1.getState().contains( \
+                        pyatspi.STATE_TRANSIENT) and \
+                    parent2.getState().contains(pyatspi.STATE_TRANSIENT)):
+                if parent1.getIndexInParent() != parent2.getIndexInParent():
+                    return False
+                parent1 = parent1.parent
+                parent2 = parent2.parent
+            if parent1 and parent2 and parent1 == parent2:
+                return self.getRealActiveDescendant(obj1).name == \
+                       self.getRealActiveDescendant(obj2).name
+        except:
+            pass
+
+        # In java applications, TRANSIENT state is missing for tree items
+        # (fix for bug #352250)
+        #
+        try:
+            parent1 = obj1
+            parent2 = obj2
+            while parent1 and parent2 and \
+                    parent1.getRole() == pyatspi.ROLE_LABEL and \
+                    parent2.getRole() == pyatspi.ROLE_LABEL:
+                if parent1.getIndexInParent() != parent2.getIndexInParent():
+                    return False
+                parent1 = parent1.parent
+                parent2 = parent2.parent
+            if parent1 and parent2 and parent1 == parent2:
+                return True
+        except:
+            pass
+
+        return False
+
+    def appendString(self, text, newText, delimiter=" "):
+        """Appends the newText to the given text with the delimiter in
+        between and returns the new string.  Edge cases, such as no
+        initial text or no newText, are handled gracefully.
+        """
+        if (not newText) or (len(newText) == 0):
+            return text
+        elif text and len(text):
+            return text + delimiter + newText
+        else:
+            return newText
+
+    def __hasLabelForRelation(self, label):
+        """Check if label has a LABEL_FOR relation
+
+        Arguments:
+        - label: the label in question
+
+        Returns TRUE if label has a LABEL_FOR relation.
+        """
+        if (not label) or (label.getRole() != pyatspi.ROLE_LABEL):
+            return False
+
+        relations = label.getRelationSet()
+
+        for relation in relations:
+            if relation.getRelationType() \
+                   == pyatspi.RELATION_LABEL_FOR:
+                return True
+
+        return False
+
+    def __isLabeling(self, label, accessible):
+        """Check if label is connected via  LABEL_FOR relation with the
+        accessible.
+
+        Arguments:
+        - label: the label in question
+        - accessible: the object in question
+
+        Returns TRUE if label has a relation LABEL_FOR for accessibleect.
+        """
+        if (not accessible) \
+           or (not label) \
+           or (label.getRole() != pyatspi.ROLE_LABEL):
+            return False
+
+        relations = label.getRelationSet()
+        if not relations:
+            return False
+
+        for relation in relations:
+            if relation.getRelationType() \
+                   == pyatspi.RELATION_LABEL_FOR:
+
+                for i in range(0, relation.getNTargets()):
+                    target = relation.getTarget(i)
+                    if target == accessible:
+                        return True
+
+        return False
+
+    def getUnicodeCurrencySymbols(self):
+        """Return a list of the unicode currency symbols, populating the list
+        if this is the first time that this routine has been called.
+
+        Returns a list of unicode currency symbols.
+        """
+        if not self._unicodeCurrencySymbols:
+            self._unicodeCurrencySymbols = [ \
+                u'\u0024',     # dollar sign
+                u'\u00A2',     # cent sign
+                u'\u00A3',     # pound sign
+                u'\u00A4',     # currency sign
+                u'\u00A5',     # yen sign
+                u'\u0192',     # latin small letter f with hook
+                u'\u060B',     # afghani sign
+                u'\u09F2',     # bengali rupee mark
+                u'\u09F3',     # bengali rupee sign
+                u'\u0AF1',     # gujarati rupee sign
+                u'\u0BF9',     # tamil rupee sign
+                u'\u0E3F',     # thai currency symbol baht
+                u'\u17DB',     # khmer currency symbol riel
+                u'\u2133',     # script capital m
+                u'\u5143',     # cjk unified ideograph-5143
+                u'\u5186',     # cjk unified ideograph-5186
+                u'\u5706',     # cjk unified ideograph-5706
+                u'\u5713',     # cjk unified ideograph-5713
+                u'\uFDFC',     # rial sign
+            ]
+
+            # Add 20A0 (EURO-CURRENCY SIGN) to 20B5 (CEDI SIGN)
+            #
+            for ordChar in range(ord(u'\u20A0'), ord(u'\u20B5') + 1):
+                self._unicodeCurrencySymbols.append(unichr(ordChar))
+
+        return self._unicodeCurrencySymbols
+
+    def findDisplayedLabel(self, accessible):
+        """Return a list of the objects that are labelling this accessible.
+
+        Argument:
+        - accessible: the object in question
+
+        Returns a list of the objects that are labelling this object.
+        """
+        # For some reason, some objects are labelled by the same thing
+        # more than once.  Go figure, but we need to check for this.
+        #
+        label = []
+        relations = accessible.getRelationSet()
+        allTargets = []
+
+        for relation in relations:
+            if relation.getRelationType() \
+                   == pyatspi.RELATION_LABELLED_BY:
+
+                # The pbject can be labelled by more than one thing, so we just
+                # get all the labels (from unique objects) and append them
+                # together.  An example of such objects live in the "Basic"
+                # page of the gnome-accessibility-keyboard-properties app.
+                # The "Delay" and "Speed" objects are labelled both by
+                # their names and units.
+                #
+                for i in range(0, relation.getNTargets()):
+                    target = relation.getTarget(i)
+                    if not target in allTargets:
+                        allTargets.append(target)
+                        label.append(target)
+
+        # [[[TODO: HACK - we've discovered oddness in hierarchies such as
+        # the gedit Edit->Preferences dialog.  In this dialog, we have
+        # labeled groupings of objects.  The grouping is done via a FILLER
+        # with two children - one child is the overall label, and the
+        # other is the container for the grouped objects.  When we detect
+        # this, we add the label to the overall context.
+        #
+        # We are also looking for objects which have a PANEL or a FILLER as
+        # parent, and its parent has more children. Through these children,
+        # a potential label with LABEL_FOR relation may exists. We want to
+        # present this label.
+        # This case can be seen in FileChooserDemo application, in Open dialog
+        # window, the line with "Look In" label, a combobox and some
+        # presentation buttons.
+        #
+        # Finally, we are searching the hierarchy of embedded components for
+        # children that are labels.]]]
+        #
+        if not len(label):
+            potentialLabels = []
+            useLabel = False
+            if (accessible.getRole() == pyatspi.ROLE_EMBEDDED):
+                candidate = accessible
+                while candidate.childCount:
+                    candidate = candidate[0]
+                # The parent of this object may contain labels
+                # or it may contain filler that contains labels.
+                #
+                candidate = candidate.parent
+                for child in candidate:
+                    if child.getRole() == pyatspi.ROLE_FILLER:
+                        candidate = child
+                        break
+                # If there are labels in this embedded component,
+                # they should be here.
+                #
+                for child in candidate:
+                    if child.getRole() == pyatspi.ROLE_LABEL:
+                        useLabel = True
+                        potentialLabels.append(child)
+            elif ((accessible.getRole() == pyatspi.ROLE_FILLER) \
+                    or (accessible.getRole() == pyatspi.ROLE_PANEL)) \
+                and (accessible.childCount == 2):
+                child0, child1 = accessible
+                child0_role = child0.getRole()
+                child1_role = child1.getRole()
+                if child0_role == pyatspi.ROLE_LABEL \
+                    and not self.__hasLabelForRelation(child0) \
+                    and child1_role in [pyatspi.ROLE_FILLER, \
+                                             pyatspi.ROLE_PANEL]:
+                    useLabel = True
+                    potentialLabels.append(child0)
+                elif child1_role == pyatspi.ROLE_LABEL \
+                    and not self.__hasLabelForRelation(child1) \
+                    and child0_role in [pyatspi.ROLE_FILLER, \
+                                             pyatspi.ROLE_PANEL]:
+                    useLabel = True
+                    potentialLabels.append(child1)
+            else:
+                parent = accessible.parent
+                if parent and \
+                    ((parent.getRole() == pyatspi.ROLE_FILLER) \
+                            or (parent.getRole() == pyatspi.ROLE_PANEL)):
+                    for potentialLabel in parent:
+                        try:
+                            useLabel = self.__isLabeling(potentialLabel, 
+                                                         accessible)
+                            if useLabel:
+                                potentialLabels.append(potentialLabel)
+                                break
+                        except:
+                            pass
+
+            if useLabel and len(potentialLabels):
+                label = potentialLabels
+
+        return label
+
+    def getDisplayedLabel(self, accessible):
+        """If there is an object labelling the given object, return the
+        text being displayed for the object labelling this object.
+        Otherwise, return None.
+
+        Argument:
+        - accessible: the object in question
+
+        Returns the string of the object labelling this object, or None
+        if there is nothing of interest here.
+        """
+        labelString = None
+        labels = self.findDisplayedLabel(accessible)
+        for label in labels:
+            labelString = self.appendString(labelString,
+                                            self.getDisplayedText(label))
+
+        return labelString
+
+    def __getDisplayedTextInComboBox(self, combo):
+
+        """Returns the text being displayed in a combo box.  If nothing is
+        displayed, then None is returned.
+
+        Arguments:
+        - combo: the combo box
+
+        Returns the text in the combo box or an empty string if nothing is
+        displayed.
+        """
+        displayedText = None
+
+        # Find the text displayed in the combo box.  This is either:
+        #
+        # 1) The last text object that's a child of the combo box
+        # 2) The selected child of the combo box.
+        # 3) The contents of the text of the combo box itself when
+        #    treated as a text object.
+        #
+        # Preference is given to #1, if it exists.
+        #
+        # If the label of the combo box is the same as the utterance for
+        # the child object, then this utterance is only displayed once.
+        #
+        # [[[TODO: WDW - Combo boxes are complex beasts.  This algorithm
+        # needs serious work.  Logged as bugzilla bug 319745.]]]
+        #
+        textObj = None
+        for child in combo:
+            if child and child.getRole() == pyatspi.ROLE_TEXT:
+                textObj = child
+
+        if textObj:
+            [displayedText, caretOffset, startOffset] = \
+                self.getTextLineAtCaret(textObj)
+            #print "TEXTOBJ", displayedText
+        else:
+            try:
+                comboSelection = combo.querySelection()
+                selectedItem = comboSelection.getSelectedChild(0)
+            except:
+                selectedItem = None
+
+            if selectedItem:
+                displayedText = self.getDisplayedText(selectedItem)
+                #print "SELECTEDITEM", displayedText
+            elif combo.name and len(combo.name):
+                # We give preference to the name over the text because
+                # the text for combo boxes seems to never change in
+                # some cases.  The main one where we see this is in
+                # the gaim "Join Chat" window.
+                #
+                displayedText = combo.name
+                #print "NAME", displayedText
+            else:
+                [displayedText, caretOffset, startOffset] = \
+                    self.getTextLineAtCaret(combo)
+                # Set to None instead of empty string.
+                displayedText = displayedText or None
+                #print "TEXT", displayedText
+
+        return displayedText
+
+    def getDisplayedText(self, accessible):
+        """Returns the text being displayed for an object.
+
+        Arguments:
+        - accessible: the object
+
+        Returns the text being displayed for an object or None if there isn't
+        any text being shown.
+        """
+        displayedText = None
+
+        role = accessible.getRole()
+        if role == pyatspi.ROLE_COMBO_BOX:
+            return self.__getDisplayedTextInComboBox(accessible)
+
+        # The accessible text of an object is used to represent what is
+        # drawn on the screen.
+        #
+        try:
+            text = accessible.queryText()
+        except NotImplementedError:
+            pass
+        else:
+            displayedText = text.getText(0, -1)
+
+            # [[[WDW - HACK to account for things such as Gecko that want
+            # to use the EMBEDDED_OBJECT_CHARACTER on a label to hold the
+            # object that has the real accessible text for the label.  We
+            # detect this by the specfic case where the text for the
+            # current object is a single EMBEDDED_OBJECT_CHARACTER.  In
+            # this case, we look to the child for the real text.]]]
+            #
+            unicodeText = displayedText.decode("UTF-8")
+            if unicodeText \
+               and (len(unicodeText) == 1) \
+               and (unicodeText[0] == self.EMBEDDED_OBJECT_CHARACTER) \
+               and accessible.childCount > 0:
+                try:
+                    displayedText = self.getDisplayedText(accessible[0])
+                except:
+                    log.exception("exception handled getting text:")
+            elif unicodeText:
+                # [[[TODO: HACK - Welll.....we'll just plain ignore any
+                # text with EMBEDDED_OBJECT_CHARACTERs here.  We still need a
+                # general case to handle this stuff and expand objects
+                # with EMBEDDED_OBJECT_CHARACTERs.]]]
+                #
+                for i in range(0, len(unicodeText)):
+                    if unicodeText[i] == self.EMBEDDED_OBJECT_CHARACTER:
+                        displayedText = None
+                        break
+
+        if not displayedText:
+            displayedText = accessible.name
+
+        # [[[WDW - HACK because push buttons can have labels as their
+        # children.  An example of this is the Font: button on the General
+        # tab in the Editing Profile dialog in gnome-terminal.
+        #
+        if not displayedText and role == pyatspi.ROLE_PUSH_BUTTON:
+            for child in accessible:
+                if child.getRole() == pyatspi.ROLE_LABEL:
+                    childText = self.getDisplayedText(child)
+                    if childText and len(childText):
+                        displayedText = self.appendString(displayedText,
+                                                          childText)
+
+        return displayedText
+
+    def getTextForValue(self, accessible):
+        """Returns the text to be displayed for the object's current value.
+
+        Arguments:
+        - accessible: the Accessible object that may or may not have a value.
+
+        Returns a string representing the value.
+        """
+        try:
+            value = accessible.queryValue()
+        except NotImplementedError:
+            return ""
+
+        # OK, this craziness is all about trying to figure out the most
+        # meaningful formatting string for the floating point values.
+        # The number of places to the right of the decimal point should
+        # be set by the minimumIncrement, but the minimumIncrement isn't
+        # always set.  So...we'll default the minimumIncrement to 1/100
+        # of the range.  But, if max == min, then we'll just go for showing
+        # them off to two meaningful digits.
+        #
+        try:
+            minimumIncrement = value.minimumIncrement
+        except:
+            minimumIncrement = 0.0
+
+        if minimumIncrement == 0.0:
+            minimumIncrement = (value.maximumValue - value.minimumValue) \
+                               / 100.0
+
+        try:
+            decimalPlaces = max(0, -math.log10(minimumIncrement))
+        except:
+            try:
+                decimalPlaces = max(0, -math.log10(value.minimumValue))
+            except:
+                try:
+                    decimalPlaces = max(0, -math.log10(value.maximumValue))
+                except:
+                    decimalPlaces = 0
+
+        formatter = "%%.%df" % decimalPlaces
+        valueString = formatter % value.currentValue
+        #minString   = formatter % value.minimumValue
+        #maxString   = formatter % value.maximumValue
+
+        # [[[TODO: WDW - probably want to do this as a percentage at some
+        # point?  Logged as bugzilla bug 319743.]]]
+        #
+        return valueString
+
+    def findFocusedAccessible(self, root):
+        """Returns the accessible that has focus under or including the
+        given root.
+
+        TODO: This will currently traverse all children, whether they are
+        visible or not and/or whether they are children of parents that
+        manage their descendants.  At some point, this method should be
+        optimized to take such things into account.
+
+        Arguments:
+        - root: the root object where to start searching
+
+        Returns the object with the FOCUSED state or None if no object with
+        the FOCUSED state can be found.
+        """
+        if root.getState().contains(pyatspi.STATE_FOCUSED):
+            return root
+
+        for child in root:
+            try:
+                candidate = self.findFocusedAccessible(child)
+                if candidate:
+                    return candidate
+            except:
+                pass
+
+        return None
+
+    def getRealActiveDescendant(self, accessible):
+        """Given an object that should be a child of an object that
+        manages its descendants, return the child that is the real
+        active descendant carrying useful information.
+
+        Arguments:
+        - accessible: an object that should be a child of an object that
+        manages its descendants.
+        """
+        # If obj is a table cell and all of it's children are table cells
+        # (probably cell renderers), then return the first child which has
+        # a non zero length text string. If no such object is found, just
+        # fall through and use the default approach below. See bug #376791
+        # for more details.
+        #
+        if accessible.getRole() == pyatspi.ROLE_TABLE_CELL \
+                                   and accessible.childCount:
+            nonTableCellFound = False
+            for child in accessible:
+                if child.getRole() != pyatspi.ROLE_TABLE_CELL:
+                    nonTableCellFound = True
+            if not nonTableCellFound:
+                for child in accessible:
+                    try:
+                        text = child.queryText()
+                    except NotImplementedError:
+                        continue
+                    else:
+                        if text.getText(0, -1):
+                            return child
+
+        # [[[TODO: WDW - this is an odd hacky thing I've somewhat drawn
+        # from Gnopernicus.  The notion here is that we get an active
+        # descendant changed event, but that object tends to have children
+        # itself and we need to decide what to do.  Well...the idea here
+        # is that the last child (Gnopernicus chooses child(1)), tends to
+        # be the child with information.  The previous children tend to
+        # be non-text or just there for spacing or something.  You will
+        # see this in the various table demos of gtk-demo and you will
+        # also see this in the Contact Source Selector in Evolution.
+        #
+        # Just note that this is most likely not a really good solution
+        # for the general case.  That needs more thought.  But, this
+        # comment is here to remind us this is being done in poor taste
+        # and we need to eventually clean up our act.]]]
+        #
+        if accessible and accessible.childCount:
+            return accessible[-1]
+        else:
+            return accessible
+
     ####################################################################
     #                                                                  #
     # AT-SPI OBJECT EVENT HANDLERS                                     #



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]