[orca] More work on Flat Review



commit 811b40d7b81ed3d55d729a69fd9834d92131293e
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Thu Sep 1 14:59:04 2016 -0400

    More work on Flat Review
    
    * Improve accuracy with respect to what is truly on screen
    * Further improve efficiency building context
    * Add toolkit-specific handling for special cases (and toolkit bugs)
    * Make menu-bar menus reviewable, including separators and disabled items
      which cannot be arrowed to natively. (Note that other menus and menu-like
      popups are not yet being addressed, but will be in a future commit.)
    * Fake the text interface for widgets whose implementation (or lack
      thereof) is toolkit dependent, and which typically display text.
      This is needed to review the displayed text by word and character.

 src/orca/flat_review.py                            |  526 ++++++--------------
 src/orca/script.py                                 |    3 -
 src/orca/script_utilities.py                       |  127 +++++-
 src/orca/scripts/default.py                        |    2 +-
 src/orca/scripts/toolkits/GAIL/script_utilities.py |    2 +
 .../scripts/toolkits/Gecko/script_utilities.py     |   29 ++
 .../keystrokes/firefox/ui_role_menu_flat_review.py |  159 ++++++
 .../gnome-terminal/man_page_flat_review.py         |    6 +-
 test/keystrokes/gtk-demo/role_menu_flat_review.py  |  105 +++--
 .../gtk-demo/role_text_multiline_flat_review.py    |    4 +-
 .../gtk3-demo/role_dialog_flat_review.py           |    4 +-
 test/keystrokes/gtk3-demo/role_menu_flat_review.py |  226 +++++++++
 .../keystrokes/gtk3-demo/role_table_flat_review.py |    8 -
 .../gtk3-demo/role_text_multiline_flat_review.py   |   12 +-
 .../gtk3-demo/role_toggle_button_flat_review.py    |   74 ++--
 .../oowriter/ui_role_menu_flat_review.py           |  160 ++++++
 16 files changed, 949 insertions(+), 498 deletions(-)
---
diff --git a/src/orca/flat_review.py b/src/orca/flat_review.py
index 6a57745..d2d1d29 100644
--- a/src/orca/flat_review.py
+++ b/src/orca/flat_review.py
@@ -93,15 +93,20 @@ class Word:
         if attr != "chars":
             return super().__getattribute__(attr)
 
+        # TODO - JD: For now, don't fake character and word extents.
+        # The main goal is to improve reviewability.
+        extents = self.x, self.y, self.width, self.height
+
         try:
             text = self.zone.accessible.queryText()
         except:
-            return []
+            text = None
 
         chars = []
         for i, char in enumerate(self.string):
             start = i + self.startOffset
-            extents = text.getRangeExtents(start, start+1, pyatspi.DESKTOP_COORDS)
+            if text:
+                extents = text.getRangeExtents(start, start+1, pyatspi.DESKTOP_COORDS)
             chars.append(Char(self, i, start, char, *extents))
 
         return chars
@@ -171,7 +176,11 @@ class Zone:
                      pyatspi.ROLE_MENU,
                      pyatspi.ROLE_MENU_ITEM,
                      pyatspi.ROLE_CHECK_MENU_ITEM,
-                     pyatspi.ROLE_RADIO_MENU_ITEM]
+                     pyatspi.ROLE_RADIO_MENU_ITEM,
+                     pyatspi.ROLE_PAGE_TAB,
+                     pyatspi.ROLE_PUSH_BUTTON,
+                     pyatspi.ROLE_TABLE_CELL]
+
         if self.role in textRoles:
             return True
 
@@ -429,9 +438,7 @@ class Line:
         return self.brailleRegions
 
 class Context:
-    """Information regarding where a user happens to be exploring
-    right now.
-    """
+    """Contains the flat review regions for the current top-level object."""
 
     ZONE   = 0
     CHAR   = 1
@@ -445,126 +452,52 @@ class Context:
     WRAP_ALL        = (WRAP_LINE | WRAP_TOP_BOTTOM)
 
     def __init__(self, script):
-        """Create a new Context that will be used for handling flat
-        review mode.
-        """
-        self.script    = script
-
-        if (not orca_state.locusOfFocus) \
-            or (orca_state.locusOfFocus.getApplication() \
-                != self.script.app):
-            self.lines = []
-        else:
-            # We want to stop at the window or frame or equivalent level.
-            #
-            obj = script.utilities.topLevelObject(orca_state.locusOfFocus)
-            if obj:
-                self.lines = self.clusterZonesByLine(self.getShowingZones(obj))
-            else:
-                self.lines = []
-
-        currentLineIndex = 0
-        currentZoneIndex = 0
-        currentWordIndex = 0
-        currentCharIndex = 0
+        """Create a new Context for script."""
+
+        self.script = script
+        self.zones = []
+        self.lines = []
+        self.lineIndex = 0
+        self.zoneIndex = 0
+        self.wordIndex = 0
+        self.charIndex = 0
+        self.targetCharInfo = None
+        self.focusZone = None
+        self.container = None
+        self.focusObj = orca_state.locusOfFocus
+        self.topLevel = script.utilities.topLevelObject(self.focusObj)
+        self.bounds = 0, 0, 0, 0
 
         try:
-            role = orca_state.locusOfFocus.getRole()
+            component = self.topLevel.queryComponent()
+            self.bounds = component.getExtents(pyatspi.DESKTOP_COORDS)
         except:
-            role = None
-
-        searchZone = orca_state.locusOfFocus
-
-        foundZoneWithFocus = False
-        while currentLineIndex < len(self.lines):
-            line = self.lines[currentLineIndex]
-            currentZoneIndex = 0
-            while currentZoneIndex < len(line.zones):
-                zone = line.zones[currentZoneIndex]
-                if searchZone in [zone.accessible, zone.accessible.parent]:
-                    foundZoneWithFocus = True
-                    break
-                else:
-                    currentZoneIndex += 1
-            if foundZoneWithFocus:
+            msg = "ERROR: Exception getting extents of %s" % self.topLevel
+            debug.println(debug.LEVEL_INFO, msg, True)
+
+        containerRoles = [pyatspi.ROLE_MENU]
+        isContainer = lambda x: x and x.getRole() in containerRoles
+        container = pyatspi.findAncestor(self.focusObj, isContainer)
+        self.container = container or self.topLevel
+
+        self.zones, self.focusZone = self.getShowingZones(self.container)
+        self.lines = self.clusterZonesByLine(self.zones)
+        if not (self.lines and self.focusZone):
+            return
+
+        for i, line in enumerate(self.lines):
+            if self.focusZone in line.zones:
+                self.lineIndex = i
+                self.zoneIndex = line.zones.index(self.focusZone)
+                word, offset = self.focusZone.wordWithCaret()
+                if word:
+                    self.wordIndex = word.index
+                    self.charIndex = offset
                 break
-            else:
-                currentLineIndex += 1
 
-        # Fallback to the first Zone if we didn't find anything.
-        #
-        if not foundZoneWithFocus:
-            currentLineIndex = 0
-            currentZoneIndex = 0
-        elif isinstance(zone, TextZone):
-            # If we're on an accessible text object, try to set the
-            # review cursor to the caret position of that object.
-            #
-            accessible  = zone.accessible
-            lineIndex   = currentLineIndex
-            zoneIndex   = currentZoneIndex
-            try:
-                caretOffset = zone.accessible.queryText().caretOffset
-            except NotImplementedError:
-                caretOffset = -1
-            foundZoneWithCaret = False
-            checkForEOF = False
-            while lineIndex < len(self.lines):
-                line = self.lines[lineIndex]
-                while zoneIndex < len(line.zones):
-                    zone = line.zones[zoneIndex]
-                    if zone.accessible == accessible:
-                        if caretOffset >= zone.startOffset:
-                            endOffset = zone.startOffset + zone.length
-                            if caretOffset < endOffset:
-                                foundZoneWithCaret = True
-                                break
-                            elif caretOffset == endOffset:
-                                checkForEOF = True
-                                lineToCheck = lineIndex
-                                zoneToCheck = zoneIndex
-                    zoneIndex += 1
-                if foundZoneWithCaret:
-                    currentLineIndex = lineIndex
-                    currentZoneIndex = zoneIndex
-                    currentWordIndex = 0
-                    currentCharIndex = 0
-                    offset = zone.startOffset
-                    while currentWordIndex < len(zone.words):
-                        word = zone.words[currentWordIndex]
-                        if (word.length + offset) > caretOffset:
-                            currentCharIndex = caretOffset - offset
-                            break
-                        else:
-                            currentWordIndex += 1
-                            offset += word.length
-                    break
-                else:
-                    zoneIndex = 0
-                    lineIndex += 1
-            atEOF = not foundZoneWithCaret and checkForEOF
-            if atEOF:
-                line = self.lines[lineToCheck]
-                zone = line.zones[zoneToCheck]
-                currentLineIndex = lineToCheck
-                currentZoneIndex = zoneToCheck
-                if caretOffset and zone.words:
-                    currentWordIndex = len(zone.words) - 1
-                    currentCharIndex = \
-                          zone.words[currentWordIndex].length - 1
-
-        self.lineIndex = currentLineIndex
-        self.zoneIndex = currentZoneIndex
-        self.wordIndex = currentWordIndex
-        self.charIndex = currentCharIndex
-
-        # This is used to tell us where we should strive to move to
-        # when going up and down lines to the closest character.
-        # The targetChar is the character where we initially started
-        # moving from, and does not change when one moves up or down
-        # by line.
-        #
-        self.targetCharInfo = None
+        msg = "FLAT REVIEW: On line %i, zone %i, word %i, char %i" % \
+              (self.lineIndex, self.zoneIndex, self.wordIndex, self.charIndex)
+        debug.println(debug.LEVEL_INFO, msg, True)
 
     def splitTextIntoZones(self, accessible, string, startOffset, cliprect):
         """Traverses the string, splitting it up into separate zones if the
@@ -664,8 +597,6 @@ class Context:
         Returns a list of Zones.
         """
 
-        debug.println(debug.LEVEL_FINEST, "  looking at text:")
-
         try:
             text = accessible.queryText()
         except NotImplementedError:
@@ -675,11 +606,16 @@ class Context:
 
         # TODO - JD: This is here temporarily whilst I sort out the rest
         # of the text-related mess.
+        if not re.search("[^\ufffc]", text.getText(0, -1)):
+            return []
+
+        # TODO - JD: Ditto.
         if "EditableText" in pyatspi.listInterfaces(accessible) \
            and accessible.getState().contains(pyatspi.STATE_SINGLE_LINE):
             extents = accessible.queryComponent().getExtents(0)
             return [TextZone(accessible, 0, text.getText(0, -1), *extents)]
 
+        debug.println(debug.LEVEL_FINEST, "  looking at text:")
 
         offset = 0
         lastEndOffset = -1
@@ -770,22 +706,6 @@ class Context:
             textZones = self.splitTextIntoZones(
                 accessible, string, startOffset, cliprect)
 
-            # We need to account for the fact that newlines at the end of 
-            # text are treated as being on the same line when they in fact
-            # are a whole separate blank line.  So, we check for this and
-            # make up a new text zone for these cases.  See bug 434654.
-            #
-            if (endOffset == length) and (string[-1:] == "\n"):
-                [x, y, width, height] = text.getRangeExtents(startOffset, 
-                                                             endOffset,
-                                                             0)
-                if not textZones:
-                    textZones = []
-                textZones.append(TextZone(accessible,
-                                          endOffset,
-                                          "",
-                                          x, y + height, 0, height))
-
             if textZones:
                 zones.extend(textZones)
             elif len(zones):
@@ -810,7 +730,7 @@ class Context:
         # TODO - JD: This whole thing is pretty hacky. Either do it
         # right or nuke it.
 
-        indicatorExtents = extents.x, extents.y, 1, extents.height
+        indicatorExtents = [extents.x, extents.y, 1, extents.height]
         role = accessible.getRole()
         if role == pyatspi.ROLE_TOGGLE_BUTTON:
             zone = StateZone(accessible, *indicatorExtents, role=role)
@@ -834,12 +754,12 @@ class Context:
         if len(zones) == 1 and isinstance(zones[0], TextZone):
             textZone = zones[0]
             textToLeftEdge = textZone.x - extents.x
-            textToRightEdge = (extents.x + extents.width) - (textZone.x + width)
+            textToRightEdge = (extents.x + extents.width) - (textZone.x + textZone.width)
             stateOnLeft = textToLeftEdge > 20
             if stateOnLeft:
                 indicatorExtents[2] = textToLeftEdge
             else:
-                indicatorExtents[0] = textZone.x + width
+                indicatorExtents[0] = textZone.x + textZone.width
                 indicatorExtents[2] = textToRightEdge
 
         zone = StateZone(accessible, *indicatorExtents, role=role)
@@ -850,12 +770,7 @@ class Context:
                 zones.append(zone)
 
     def getZonesFromAccessible(self, accessible, cliprect):
-        """Returns a list of Zones for the given accessible.
-
-        Arguments:
-        - accessible: the accessible
-        - cliprect: the extents that the Zones must fit inside.
-        """
+        """Returns a list of Zones for the given accessible."""
 
         try:
             component = accessible.queryComponent()
@@ -863,163 +778,52 @@ class Context:
         except:
             return []
 
-        if not self.script.utilities.containsRegion(extents, cliprect):
-            return []
-
         try:
             role = accessible.getRole()
-            childCount = accessible.childCount
         except:
             return []
 
-        try:
-            accessible.queryText()
-        except NotImplementedError:
-            zones = []
-        else:
-            zones = self.getZonesFromText(accessible, cliprect)
-
-        clipping = self.script.utilities.intersection(extents, cliprect)
+        zones = self.getZonesFromText(accessible, cliprect)
         if not zones and role in [pyatspi.ROLE_SCROLL_BAR,
                                   pyatspi.ROLE_SLIDER,
                                   pyatspi.ROLE_PROGRESS_BAR]:
-            zones.append(ValueZone(accessible, *clipping))
-        elif childCount and role not in [pyatspi.ROLE_COMBO_BOX,
-                                         pyatspi.ROLE_EMBEDDED,
-                                         pyatspi.ROLE_LABEL,
-                                         pyatspi.ROLE_MENU,
-                                         pyatspi.ROLE_PAGE_TAB]:
-            pass
+            zones.append(ValueZone(accessible, *extents))
         elif not zones:
             string = self.script.speechGenerator.getName(accessible)
             useless = [pyatspi.ROLE_TABLE_CELL, pyatspi.ROLE_LABEL]
             if not string and role not in useless:
-                string = self.script.speechGenerator.getLocalizedRoleName(accessible)
-
-            # TODO - JD: This check will become obsolete soon.
-            if string and (clipping[2] or clipping[3] != 0):
-                zones.append(Zone(accessible, string, *clipping))
+                string = self.script.speechGenerator.getRoleName(accessible)
+            if string:
+                zones.append(Zone(accessible, string, *extents))
 
         self._insertStateZone(zones, accessible, extents)
 
         return zones
 
-    def getShowingZones(self, root):
-        """Returns a list of all interesting, non-intersecting, regions
-        that are drawn on the screen.  Each element of the list is the
-        Accessible object associated with a given region.  The term
-        'zone' here is inherited from OCR algorithms and techniques.
-
-        The Zones are returned in no particular order.
-
-        Arguments:
-        - root: the Accessible object to traverse
-
-        Returns: a list of Zones under the specified object
-        """
-
-        if not root:
-            return []
-
-        zones = []
-        try:
-            rootexts = root.queryComponent().getExtents(0)
-        except:
-            return []
-
-        rootrole = root.getRole()
-
-        # If we're at a leaf node, then we've got a good one on our hands.
-        #
-        try:
-            childCount = root.childCount
-        except (LookupError, RuntimeError):
-            childCount = -1
-        if root.childCount <= 0:
-            return self.getZonesFromAccessible(root, rootexts)
-
-        # Handle non-leaf Java JTree nodes. If the node is collapsed,
-        # treat it as a leaf node. If it's expanded, add it to the
-        # Zones list.
-        #
-        stateset = root.getState()
-        if stateset.contains(pyatspi.STATE_EXPANDABLE):
-            if stateset.contains(pyatspi.STATE_COLLAPSED):
-                return self.getZonesFromAccessible(root, rootexts)
-            elif stateset.contains(pyatspi.STATE_EXPANDED):
-                treenode = self.getZonesFromAccessible(root, rootexts)
-                if treenode:
-                    zones.extend(treenode)
-
-        # We'll stop at various objects because, while they do have
-        # children, we logically think of them as one region on the
-        # screen.  [[[TODO: WDW - HACK stopping at menu bars for now
-        # because their menu items tell us they are showing even though
-        # they are not showing.  Until I can figure out a reliable way to
-        # get past these lies, I'm going to ignore them.]]]
-        #
-        if (root.parent and (root.parent.getRole() == pyatspi.ROLE_MENU_BAR)) \
-           or (rootrole == pyatspi.ROLE_COMBO_BOX) \
-           or (rootrole == pyatspi.ROLE_EMBEDDED) \
-           or (rootrole == pyatspi.ROLE_TEXT) \
-           or (rootrole == pyatspi.ROLE_SCROLL_BAR):
-            return self.getZonesFromAccessible(root, rootexts)
-
-        # If this is a status bar, only pursue its children if we cannot
-        # get non-empty text information from the status bar.
-        # See bug #506874 for more details.
-        #
-        if rootrole == pyatspi.ROLE_STATUS_BAR:
-            zones = self.getZonesFromText(root, rootexts)
-            if len(zones):
-                return zones
+    def getShowingZones(self, root, boundingbox=None):
+        """Returns an unsorted list of all the zones under root and the focusZone."""
 
-        # Otherwise, dig deeper.
-        #
-        # We'll include page tabs: while they are parents, their extents do
-        # not contain their children. [[[TODO: WDW - need to consider all
-        # parents, especially those that implement accessible text.  Logged
-        # as bugzilla bug 319773.]]]
-        #
-        if rootrole == pyatspi.ROLE_PAGE_TAB:
-            zones.extend(self.getZonesFromAccessible(root, rootexts))
+        if boundingbox is None:
+            boundingbox = self.bounds
 
-        try:
-            root.queryText()
-            if len(zones) == 0:
-                zones = self.getZonesFromAccessible(root, rootexts)
-        except NotImplementedError:
-            pass
+        objs = self.script.utilities.getOnScreenObjects(root, boundingbox)
+        msg = "FLAT REVIEW: %i on-screen objects found for %s" % (len(objs), root)
+        debug.println(debug.LEVEL_INFO, msg, True)
 
-        cells = None
-        if "Table" in pyatspi.listInterfaces(root):
-            cells = self.script.utilities.getVisibleTableCells(root, rootexts)
-        if cells:
-            for cell in cells:
-                zones.extend(self.getShowingZones(cell))
-        else:
-            for i in range(0, root.childCount):
-                child = root.getChildAtIndex(i)
-                if child == root:
-                    debug.println(debug.LEVEL_WARNING,
-                                  "flat_review.getShowingZones: " +
-                                  "WARNING CHILD == PARENT!!!")
-                    continue
-                elif not child:
-                    debug.println(debug.LEVEL_WARNING,
-                                  "flat_review.getShowingZones: " +
-                                  "WARNING CHILD IS NONE!!!")
-                    continue
-                elif child.parent != root:
-                    debug.println(debug.LEVEL_WARNING,
-                                  "flat_review.getShowingZones: " +
-                                  "WARNING CHILD.PARENT != PARENT!!!")
-                                  
-                if self.script.utilities.pursueForFlatReview(child):
-                    zones.extend(self.getShowingZones(child))
+        allZones, focusZone = [], None
+        for o in objs:
+            zones = self.getZonesFromAccessible(o, boundingbox)
+            if not zones:
+                continue
 
-        return zones
+            allZones.extend(zones)
+            if zones and (o == self.focusObj or o in self.focusObj):
+                zones = list(filter(lambda z: z.hasCaret(), zones)) or zones
+                focusZone = zones[0]
 
+        msg = "FLAT REVIEW: %i zones found for %s" % (len(allZones), root)
+        debug.println(debug.LEVEL_INFO, msg, True)
+        return allZones, focusZone
 
     def clusterZonesByLine(self, zones):
         """Returns a sorted list of Line clusters containing sorted Zones."""
@@ -1047,8 +851,30 @@ class Context:
                 zone.line = lines[lineIndex]
                 zone.index = zoneIndex
 
+        msg = "FLAT REVIEW: Zones clustered into %i lines" % len(lines)
+        debug.println(debug.LEVEL_INFO, msg, True)
         return lines
 
+    def getCurrent(self, flatReviewType=ZONE):
+        """Returns the current string, offset, and extent information."""
+
+        # TODO - JD: This method has not (yet) been renamed. But we have a
+        # getter and setter which do totally different things....
+
+        zone = self._getCurrentZone()
+        if not zone:
+            return None, -1, -1, -1, -1
+
+        current = zone
+        if flatReviewType == Context.LINE:
+            current = zone.line
+        elif zone.words:
+            current = zone.words[self.wordIndex]
+            if flatReviewType == Context.CHAR and current.chars:
+                current = current.chars[self.charIndex]
+
+        return current.string, current.x, current.y, current.width, current.height
+
     def setCurrent(self, lineIndex, zoneIndex, wordIndex, charIndex):
         """Sets the current character of interest.
 
@@ -1065,123 +891,55 @@ class Context:
         self.charIndex = charIndex
         self.targetCharInfo = self.getCurrent(Context.CHAR)
 
-        #print "Current line=%d zone=%d word=%d char=%d" \
-        #      % (lineIndex, zoneIndex, wordIndex, charIndex)
+    def _getClickPoint(self):
+        string, x, y, width, height = self.getCurrent(Context.CHAR)
+        if x < 0 and y < 0:
+            return -1, -1
+
+        # Click left of center to position the caret there.
+        x = int(max(x, x + (width / 2) - 1))
+        y = int(y + height / 2)
+
+        return x, y
 
     def routeToCurrent(self):
         """Routes the mouse pointer to the current accessible."""
 
-        if (not self.lines) \
-           or (not self.lines[self.lineIndex].zones):
-            return
+        x, y = self._getClickPoint()
+        if x < 0 or y < 0:
+            return False
 
-        [string, x, y, width, height] = self.getCurrent(Context.CHAR)
-        try:
-            # We try to move to the left of center.  This is to
-            # handle toolkits that will offset the caret position to
-            # the right if you click dead on center of a character.
-            #
-            x = max(x, x + (width / 2) - 1)
-            eventsynthesizer.routeToPoint(x, y + height / 2, "abs")
-        except:
-            debug.printException(debug.LEVEL_SEVERE)
+        eventsynthesizer.routeToPoint(x, y, "abs")
+        return True
 
     def clickCurrent(self, button=1):
         """Performs a mouse click on the current accessible."""
 
-        if (not self.lines) \
-           or (not self.lines[self.lineIndex].zones):
-            return
-
-        [string, x, y, width, height] = self.getCurrent(Context.CHAR)
-        try:
-
-            # We try to click to the left of center.  This is to
-            # handle toolkits that will offset the caret position to
-            # the right if you click dead on center of a character.
-            #
-            x = max(x, x + (width / 2) - 1)
-            eventsynthesizer.clickPoint(x,
-                                        y + height / 2,
-                                        button)
-        except:
-            debug.printException(debug.LEVEL_SEVERE)
-
-    def getCurrentAccessible(self):
-        """Returns the accessible associated with the current locus of
-        interest.
-        """
-
-        if (not self.lines) \
-           or (not self.lines[self.lineIndex].zones):
-            return [None, -1, -1, -1, -1]
-
-        zone = self.lines[self.lineIndex].zones[self.zoneIndex]
+        x, y = self._getClickPoint()
+        if x < 0 or y < 0:
+            return False
 
-        return zone.accessible
+        eventsynthesizer.clickPoint(x, y, button)
+        return True
 
-    def getCurrent(self, flatReviewType=ZONE):
-        """Gets the string, offset, and extent information for the
-        current locus of interest.
+    def _getCurrentZone(self):
+        if not (self.lines and 0 <= self.lineIndex < len(self.lines)):
+            return None
 
-        Arguments:
-        - flatReviewType: one of ZONE, CHAR, WORD, LINE
+        line = self.lines[self.lineIndex]
+        if not (line and 0 <= self.zoneIndex < len(line.zones)):
+            return None
 
-        Returns: [string, x, y, width, height]
-        """
+        return line.zones[self.zoneIndex]
 
-        if (not self.lines) \
-           or (not self.lines[self.lineIndex].zones):
-            return [None, -1, -1, -1, -1]
+    def getCurrentAccessible(self):
+        """Returns the current accessible."""
 
-        zone = self.lines[self.lineIndex].zones[self.zoneIndex]
+        zone = self._getCurrentZone()
+        if not zone:
+            return None
 
-        if flatReviewType == Context.ZONE:
-            return [zone.string,
-                    zone.x,
-                    zone.y,
-                    zone.width,
-                    zone.height]
-        elif flatReviewType == Context.CHAR:
-            if isinstance(zone, TextZone):
-                words = zone.words
-                if words:
-                    chars = zone.words[self.wordIndex].chars
-                    if chars:
-                        char = chars[self.charIndex]
-                        return [char.string,
-                                char.x,
-                                char.y,
-                                char.width,
-                                char.height]
-                    else:
-                        word = words[self.wordIndex]
-                        return [word.string,
-                                word.x,
-                                word.y,
-                                word.width,
-                                word.height]
-            return self.getCurrent(Context.ZONE)
-        elif flatReviewType == Context.WORD:
-            if isinstance(zone, TextZone):
-                words = zone.words
-                if words:
-                    word = words[self.wordIndex]
-                    return [word.string,
-                            word.x,
-                            word.y,
-                            word.width,
-                            word.height]
-            return self.getCurrent(Context.ZONE)
-        elif flatReviewType == Context.LINE:
-            line = self.lines[self.lineIndex]
-            return [line.string,
-                    line.x,
-                    line.y,
-                    line.width,
-                    line.height]
-        else:
-            raise Exception("Invalid type: %d" % flatReviewType)
+        return zone.accessible
 
     def getCurrentBrailleRegions(self):
         """Gets the braille for the entire current line.
diff --git a/src/orca/script.py b/src/orca/script.py
index 7eff80b..bf8379c 100644
--- a/src/orca/script.py
+++ b/src/orca/script.py
@@ -44,7 +44,6 @@ import pyatspi
 from . import braille_generator
 from . import debug
 from . import event_manager
-from . import flat_review
 from . import formatting
 from . import label_inference
 from . import keybindings
@@ -120,8 +119,6 @@ class Script:
         self.spellcheck = self.getSpellCheck()
         self.tutorialGenerator = self.getTutorialGenerator()
 
-        self.flatReviewContextClass = flat_review.Context
-
         self.findCommandRun = False
         self._lastCommandWasStructNav = False
 
diff --git a/src/orca/script_utilities.py b/src/orca/script_utilities.py
index 15c0fe6..5046bc4 100644
--- a/src/orca/script_utilities.py
+++ b/src/orca/script_utilities.py
@@ -1548,33 +1548,101 @@ class Utilities:
         self._script.generatorCache[self.NODE_LEVEL][obj] = len(nodes) - 1
         return self._script.generatorCache[self.NODE_LEVEL][obj]
 
-    def pursueForFlatReview(self, obj):
-        """Determines if we should look any further at the object
-        for flat review."""
+    def isOnScreen(self, obj, boundingbox=None):
+        if self.isDead(obj):
+            return False
 
         if self.isHidden(obj):
             return False
 
+        if not self.isShowingAndVisible(obj):
+            msg = "INFO: %s is not showing and visible" % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return False
+
         try:
-            role = obj.getRole()
-            state = obj.getState()
-            childCount = obj.childCount
+            box = obj.queryComponent().getExtents(pyatspi.DESKTOP_COORDS)
         except:
-            msg = "ERROR: Exception getting role, state, childCount of %s" % obj
+            msg = "ERROR: Exception getting extents for %s" % obj
             debug.println(debug.LEVEL_INFO, msg, True)
             return False
 
-        if not state.contains(pyatspi.STATE_SHOWING):
+        if box.x < 0 and box.y < 0:
+            msg = "INFO: %s has negative coordinates" % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
             return False
 
-        containers = [pyatspi.ROLE_LIST_BOX,
-                      pyatspi.ROLE_PANEL]
+        if not (box.width or box.height):
+            if not obj.childCount:
+                msg = "INFO: %s has no size and no children" % obj
+                debug.println(debug.LEVEL_INFO, msg, True)
+                return False
+            return True
 
-        if role in containers and not childCount:
+        if boundingbox is None or not self._boundsIncludeChildren(obj.parent):
+            return True
+
+        if not self.containsRegion(box, boundingbox):
+            msg = "INFO: %s %s not in %s" % (obj, box, boundingbox)
+            debug.println(debug.LEVEL_INFO, msg, True)
             return False
 
         return True
 
+    def getOnScreenObjects(self, root, extents=None):
+        if not self.isOnScreen(root, extents):
+            return []
+
+        try:
+            role = root.getRole()
+        except:
+            msg = "ERROR: Exception getting role of %s" % root
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return []
+
+        if role == pyatspi.ROLE_INVALID:
+            return []
+
+        if role == pyatspi.ROLE_COMBO_BOX:
+            return [root]
+
+        if extents is None:
+            try:
+                component = root.queryComponent()
+                extents = component.getExtents(pyatspi.DESKTOP_COORDS)
+            except:
+                msg = "ERROR: Exception getting extents of %s" % root
+                debug.println(debug.LEVEL_INFO, msg, True)
+                extents = 0, 0, 0, 0
+
+        interfaces = pyatspi.listInterfaces(root)
+        if 'Table' in interfaces and 'Selection' in interfaces:
+            visibleCells = self.getVisibleTableCells(root)
+            if visibleCells:
+                return visibleCells
+
+        nonText = [pyatspi.ROLE_STATUS_BAR, pyatspi.ROLE_UNKNOWN]
+        objects = []
+        if (role == pyatspi.ROLE_PAGE_TAB and root.name) \
+           or (role not in nonText and "Text" in pyatspi.listInterfaces(root)):
+            objects.append(root)
+
+        for child in root:
+            objects.extend(self.getOnScreenObjects(child, extents))
+
+        if objects:
+            return objects
+
+        containers = [pyatspi.ROLE_FILLER,
+                      pyatspi.ROLE_LIST_BOX,
+                      pyatspi.ROLE_PANEL,
+                      pyatspi.ROLE_SCROLL_PANE,
+                      pyatspi.ROLE_VIEWPORT]
+        if role in containers:
+            return []
+
+        return [root]
+
     @staticmethod
     def isTableRow(obj):
         """Determines if obj is a table row -- real or functionally."""
@@ -3465,12 +3533,20 @@ class Utilities:
 
         return rows
 
-    def getVisibleTableCells(self, obj, extents):
+    def getVisibleTableCells(self, obj):
         try:
             table = obj.queryTable()
         except:
             return []
 
+        try:
+            component = obj.queryComponent()
+            extents = component.getExtents(pyatspi.DESKTOP_COORDS)
+        except:
+            msg = "ERROR: Exception getting extents of %s" % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return []
+
         rows = self.visibleRows(obj, extents)
         if not rows:
             return []
@@ -3489,7 +3565,7 @@ class Utilities:
                     cell = table.getAccessibleAt(row, col)
                 except:
                     continue
-                if cell:
+                if cell and self.isOnScreen(cell):
                     cells.append(cell)
 
         return cells
@@ -3624,6 +3700,31 @@ class Utilities:
         debug.println(debug.LEVEL_INFO, msg, True)
         return False
 
+    def isShowingAndVisible(self, obj):
+        try:
+            state = obj.getState()
+        except:
+            msg = "ERROR: Exception getting state of %s" % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return False
+
+        if state.contains(pyatspi.STATE_SHOWING) \
+           and state.contains(pyatspi.STATE_VISIBLE):
+            return True
+
+        # TODO - JD: This really should be in the toolkit scripts. But it
+        # seems to be present in multiple toolkits, so it's either being
+        # inherited (e.g. from Gtk in Firefox Chrome, LO, Eclipse) or it
+        # may be an AT-SPI2 bug. For now, handling it here.
+        isMenuBar = lambda x: x and x.getRole() == pyatspi.ROLE_MENU_BAR
+        result = pyatspi.findAncestor(obj, isMenuBar) is not None
+        if result:
+            msg = "HACK: Treating %s as showing and visible" % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return True
+
+        return False
+
     def isDead(self, obj):
         try:
             name = obj.name
diff --git a/src/orca/scripts/default.py b/src/orca/scripts/default.py
index 82513c2..6d5f4cc 100644
--- a/src/orca/scripts/default.py
+++ b/src/orca/scripts/default.py
@@ -3306,7 +3306,7 @@ class Script(script.Script):
         """Returns the flat review context, creating one if necessary."""
 
         if not self.flatReviewContext:
-            self.flatReviewContext = self.flatReviewContextClass(self)
+            self.flatReviewContext = flat_review.Context(self)
             self.justEnteredFlatReviewMode = True
 
             # Remember where the cursor currently was
diff --git a/src/orca/scripts/toolkits/GAIL/script_utilities.py 
b/src/orca/scripts/toolkits/GAIL/script_utilities.py
index 2c61fdf..07463d1 100644
--- a/src/orca/scripts/toolkits/GAIL/script_utilities.py
+++ b/src/orca/scripts/toolkits/GAIL/script_utilities.py
@@ -25,8 +25,10 @@ __date__      = "$Date$"
 __copyright__ = "Copyright (c) 2014 Igalia, S.L."
 __license__   = "LGPL"
 
+import pyatspi
 import re
 
+import orca.debug as debug
 import orca.script_utilities as script_utilities
 
 class Utilities(script_utilities.Utilities):
diff --git a/src/orca/scripts/toolkits/Gecko/script_utilities.py 
b/src/orca/scripts/toolkits/Gecko/script_utilities.py
index ac80920..07823ff 100644
--- a/src/orca/scripts/toolkits/Gecko/script_utilities.py
+++ b/src/orca/scripts/toolkits/Gecko/script_utilities.py
@@ -45,6 +45,12 @@ class Utilities(web.Utilities):
     def _attemptBrokenTextRecovery(self):
         return True
 
+    def _treatAsLeafNode(self, obj):
+        if obj.getRole() == pyatspi.ROLE_TABLE_ROW:
+            return not obj.childCount
+
+        return super()._treatAsLeafNode(obj)
+
     def containsPoint(self, obj, x, y, coordType):
         if not super().containsPoint(obj, x, y, coordType):
             return False
@@ -103,3 +109,26 @@ class Utilities(web.Utilities):
         msg = "GECKO: Treating %s and %s as same object: %s" % (obj1, obj2, rv)
         debug.println(debug.LEVEL_INFO, msg, True)
         return rv
+
+    def isOnScreen(self, obj, boundingbox=None):
+        if not super().isOnScreen(obj, boundingbox):
+            return False
+        if obj.getRole() != pyatspi.ROLE_UNKNOWN:
+            return True
+
+        if self.topLevelObject(obj) == obj.parent:
+            msg = "INFO: %s is suspected to be off screen object" % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return False
+
+        return True
+
+    def getOnScreenObjects(self, root, extents=None):
+        objects = super().getOnScreenObjects(root, extents)
+
+        # For things like Thunderbird's "Select columns to display" button
+        if root.getRole() == pyatspi.ROLE_TREE_TABLE and root.childCount:
+            isExtra = lambda x: x and x.getRole() != pyatspi.ROLE_COLUMN_HEADER
+            objects.extend([x for x in root[0] if isExtra(x)])
+
+        return objects
diff --git a/test/keystrokes/firefox/ui_role_menu_flat_review.py 
b/test/keystrokes/firefox/ui_role_menu_flat_review.py
new file mode 100644
index 0000000..9bd655f
--- /dev/null
+++ b/test/keystrokes/firefox/ui_role_menu_flat_review.py
@@ -0,0 +1,159 @@
+#!/usr/bin/python
+
+"""Test of menu and menu item output."""
+
+from macaroon.playback import *
+import utils
+
+sequence = MacroSequence()
+
+sequence.append(PauseAction(3000))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("<Alt>v"))
+sequence.append(utils.AssertPresentationAction(
+    "1. Initial menu and menu item",
+    ["BRAILLE LINE:  'Firefox application Nightly frame Menu Bar tool bar Application menu bar Toolbars 
menu'",
+     "     VISIBLE:  'Toolbars menu', cursor=1",
+     "SPEECH OUTPUT: 'Menu Bar tool bar'",
+     "SPEECH OUTPUT: 'View menu'",
+     "SPEECH OUTPUT: 'Toolbars menu.'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("Down"))
+sequence.append(utils.AssertPresentationAction(
+    "2. Down",
+    ["BRAILLE LINE:  'Firefox application Nightly frame Menu Bar tool bar Application menu bar Sidebar 
menu'",
+     "     VISIBLE:  'Sidebar menu', cursor=1",
+     "SPEECH OUTPUT: 'Sidebar menu.'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_8"))
+sequence.append(utils.AssertPresentationAction(
+    "3. Review currernt line",
+    ["BRAILLE LINE:  'Sidebar $l'",
+     "     VISIBLE:  'Sidebar $l', cursor=1",
+     "SPEECH OUTPUT: 'Sidebar'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "4. Review next line",
+    ["BRAILLE LINE:  'separator $l'",
+     "     VISIBLE:  'separator $l', cursor=1",
+     "SPEECH OUTPUT: 'separator'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "5. Review next line",
+    ["BRAILLE LINE:  'Zoom $l'",
+     "     VISIBLE:  'Zoom $l', cursor=1",
+     "SPEECH OUTPUT: 'Zoom'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "6. Review next line",
+    ["BRAILLE LINE:  'Page Style $l'",
+     "     VISIBLE:  'Page Style $l', cursor=1",
+     "SPEECH OUTPUT: 'Page Style'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "7. Review next line",
+    ["BRAILLE LINE:  'Text Encoding $l'",
+     "     VISIBLE:  'Text Encoding $l', cursor=1",
+     "SPEECH OUTPUT: 'Text Encoding'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_5"))
+sequence.append(utils.AssertPresentationAction(
+    "8. Review current word",
+    ["BRAILLE LINE:  'Text Encoding $l'",
+     "     VISIBLE:  'Text Encoding $l', cursor=1",
+     "SPEECH OUTPUT: 'Text '"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_6"))
+sequence.append(utils.AssertPresentationAction(
+    "9. Review next word",
+    ["BRAILLE LINE:  'Text Encoding $l'",
+     "     VISIBLE:  'Text Encoding $l', cursor=6",
+     "SPEECH OUTPUT: 'Encoding'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_2"))
+sequence.append(utils.AssertPresentationAction(
+    "10. Review current char",
+    ["BRAILLE LINE:  'Text Encoding $l'",
+     "     VISIBLE:  'Text Encoding $l', cursor=6",
+     "SPEECH OUTPUT: 'E'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_3"))
+sequence.append(utils.AssertPresentationAction(
+    "11. Review next char",
+    ["BRAILLE LINE:  'Text Encoding $l'",
+     "     VISIBLE:  'Text Encoding $l', cursor=7",
+     "SPEECH OUTPUT: 'n'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_6"))
+sequence.append(utils.AssertPresentationAction(
+    "12. Review next word",
+    ["BRAILLE LINE:  'separator $l'",
+     "     VISIBLE:  'separator $l', cursor=1",
+     "SPEECH OUTPUT: 'separator'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_2"))
+sequence.append(utils.AssertPresentationAction(
+    "13. Review current char",
+    ["BRAILLE LINE:  'separator $l'",
+     "     VISIBLE:  'separator $l', cursor=1",
+     "SPEECH OUTPUT: 'separator'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_3"))
+sequence.append(utils.AssertPresentationAction(
+    "14. Review next char",
+    ["BRAILLE LINE:  '< > Full Screen $l'",
+     "     VISIBLE:  '< > Full Screen $l', cursor=1",
+     "SPEECH OUTPUT: 'not checked'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_1"))
+sequence.append(utils.AssertPresentationAction(
+    "15. Review previous char",
+    ["BRAILLE LINE:  'separator $l'",
+     "     VISIBLE:  'separator $l', cursor=1",
+     "SPEECH OUTPUT: 'separator'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_1"))
+sequence.append(utils.AssertPresentationAction(
+    "16. Review previous char",
+    ["BRAILLE LINE:  'Text Encoding $l'",
+     "     VISIBLE:  'Text Encoding $l', cursor=13",
+     "SPEECH OUTPUT: 'g'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_4"))
+sequence.append(utils.AssertPresentationAction(
+    "17. Review previous word",
+    ["BRAILLE LINE:  'Text Encoding $l'",
+     "     VISIBLE:  'Text Encoding $l', cursor=1",
+     "SPEECH OUTPUT: 'Text '"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_7"))
+sequence.append(utils.AssertPresentationAction(
+    "18. Review previous line",
+    ["BRAILLE LINE:  'Page Style $l'",
+     "     VISIBLE:  'Page Style $l', cursor=1",
+     "SPEECH OUTPUT: 'Page Style'"]))
+
+sequence.append(utils.AssertionSummaryAction())
+sequence.start()
diff --git a/test/keystrokes/gnome-terminal/man_page_flat_review.py 
b/test/keystrokes/gnome-terminal/man_page_flat_review.py
index 1dadbe3..7652b0f 100644
--- a/test/keystrokes/gnome-terminal/man_page_flat_review.py
+++ b/test/keystrokes/gnome-terminal/man_page_flat_review.py
@@ -15,7 +15,7 @@ sequence.append(KeyComboAction("KP_8"))
 sequence.append(utils.AssertPresentationAction(
     "1. Review current line",
     ["BRAILLE LINE:  ' Manual page orca(1) line 1 (press h for help or q to quit) $l'",
-     "     VISIBLE:  '(press h for help or q to quit) ', cursor=32",
+     "     VISIBLE:  ' (press h for help or q to quit)', cursor=32",
      "SPEECH OUTPUT: ' Manual page orca(1) line 1 (press h for help or q to quit)",
      "'"]))
 
@@ -140,7 +140,7 @@ sequence.append(utils.AssertPresentationAction(
     "12. Review current line",
     ["KNOWN ISSUE: We currently deliberately exit flat review and return to the bottom of the window",
     "BRAILLE LINE:  ' Manual page orca(1) line 24 (press h for help or q to quit) $l'",
-     "     VISIBLE:  '(press h for help or q to quit) ', cursor=32",
+     "     VISIBLE:  ' (press h for help or q to quit)', cursor=32",
      "SPEECH OUTPUT: ' Manual page orca(1) line 24 (press h for help or q to quit)",
      "'"]))
 
@@ -237,7 +237,7 @@ sequence.append(KeyComboAction("KP_8"))
 sequence.append(utils.AssertPresentationAction(
     "20. Review current line",
     ["BRAILLE LINE:  ' Manual page orca(1) line 1 (press h for help or q to quit) $l'",
-     "     VISIBLE:  '(press h for help or q to quit) ', cursor=32",
+     "     VISIBLE:  ' (press h for help or q to quit)', cursor=32",
      "SPEECH OUTPUT: ' Manual page orca(1) line 1 (press h for help or q to quit)",
      "'"]))
 
diff --git a/test/keystrokes/gtk-demo/role_menu_flat_review.py 
b/test/keystrokes/gtk-demo/role_menu_flat_review.py
index 92d44b9..478cd05 100644
--- a/test/keystrokes/gtk-demo/role_menu_flat_review.py
+++ b/test/keystrokes/gtk-demo/role_menu_flat_review.py
@@ -11,6 +11,8 @@ sequence.append(KeyComboAction("<Control>f"))
 sequence.append(TypeAction("Application main window"))
 sequence.append(KeyComboAction("Return"))
 sequence.append(KeyComboAction("Return"))
+sequence.append(KeyComboAction("<Control>r"))
+sequence.append(PauseAction(3000))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("<Alt>p"))
@@ -27,112 +29,127 @@ sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_8"))
 sequence.append(utils.AssertPresentationAction(
     "2. Review current line",
-    ["KNOWN ISSUE: Menu items are not currently reviewable. Thus all the following assertions are wrong.",
-     "BRAILLE LINE:  'File Preferences Help $l'",
-     "     VISIBLE:  'File Preferences Help $l', cursor=1",
-     "SPEECH OUTPUT: 'File Preferences Help'"]))
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=10",
+     "SPEECH OUTPUT: 'checked Bold not checked Blue'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_7"))
 sequence.append(utils.AssertPresentationAction(
     "3. Review previous line",
-    [""]))
+    ["BRAILLE LINE:  'Shape < > Green $l'",
+     "     VISIBLE:  'Shape < > Green $l', cursor=1",
+     "SPEECH OUTPUT: 'Shape not checked Green'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_9"))
 sequence.append(utils.AssertPresentationAction(
     "4. Review next line",
-    ["BRAILLE LINE:  'Open & y toggle button Quit GTK! $l'",
-     "     VISIBLE:  'Open & y toggle button Quit GTK!', cursor=1",
-     "SPEECH OUTPUT: 'Open not pressed toggle button Quit GTK!'"]))
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=1",
+     "SPEECH OUTPUT: 'checked Bold not checked Blue'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_5"))
 sequence.append(utils.AssertPresentationAction(
     "5. Review current word",
-    ["BRAILLE LINE:  'Open & y toggle button Quit GTK! $l'",
-     "     VISIBLE:  'Open & y toggle button Quit GTK!', cursor=1",
-     "SPEECH OUTPUT: 'Open'"]))
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=1",
+     "SPEECH OUTPUT: 'checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_6"))
 sequence.append(utils.AssertPresentationAction(
     "6. Review next word",
-    ["BRAILLE LINE:  'Open & y toggle button Quit GTK! $l'",
-     "     VISIBLE:  'Open & y toggle button Quit GTK!', cursor=6",
-     "SPEECH OUTPUT: 'not pressed'"]))
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=5",
+     "SPEECH OUTPUT: 'Bold'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_9"))
 sequence.append(utils.AssertPresentationAction(
     "7. Review next line",
-    ["BRAILLE LINE:  'text $l'",
-     "     VISIBLE:  'text $l', cursor=1",
-     "SPEECH OUTPUT: 'text'"]))
+    [""]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_5"))
 sequence.append(utils.AssertPresentationAction(
     "8. Review current word",
-    ["BRAILLE LINE:  'text $l'",
-     "     VISIBLE:  'text $l', cursor=1",
-     "SPEECH OUTPUT: 'text'"]))
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=5",
+     "SPEECH OUTPUT: 'Bold'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_2"))
 sequence.append(utils.AssertPresentationAction(
     "9. Review current char",
-    ["BRAILLE LINE:  'text $l'",
-     "     VISIBLE:  'text $l', cursor=1",
-     "SPEECH OUTPUT: 'text'"]))
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=5",
+     "SPEECH OUTPUT: 'B'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_6"))
 sequence.append(utils.AssertPresentationAction(
     "10. Review next word",
-    ["BRAILLE LINE:  'Cursor at row 0 column 0 - 0 chars in document $l'",
-     "     VISIBLE:  'Cursor at row 0 column 0 - 0 cha', cursor=1",
-     "SPEECH OUTPUT: 'Cursor '"]))
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=10",
+     "SPEECH OUTPUT: 'not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_3"))
 sequence.append(utils.AssertPresentationAction(
     "11. Review next char",
-    ["BRAILLE LINE:  'Cursor at row 0 column 0 - 0 chars in document $l'",
-     "     VISIBLE:  'Cursor at row 0 column 0 - 0 cha', cursor=2",
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=14",
+     "SPEECH OUTPUT: 'B'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_3"))
+sequence.append(utils.AssertPresentationAction(
+    "12. Review next char",
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=15",
+     "SPEECH OUTPUT: 'l'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_3"))
+sequence.append(utils.AssertPresentationAction(
+    "13. Review next char",
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=16",
      "SPEECH OUTPUT: 'u'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_1"))
 sequence.append(utils.AssertPresentationAction(
-    "12. Review previous char",
-    ["BRAILLE LINE:  'Cursor at row 0 column 0 - 0 chars in document $l'",
-     "     VISIBLE:  'Cursor at row 0 column 0 - 0 cha', cursor=1",
-     "SPEECH OUTPUT: 'C'"]))
+    "14. Review previous char",
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=15",
+     "SPEECH OUTPUT: 'l'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_1"))
 sequence.append(utils.AssertPresentationAction(
-    "13. Review previous char",
-    ["BRAILLE LINE:  'text $l'",
-     "     VISIBLE:  'text $l', cursor=1",
-     "SPEECH OUTPUT: 'text'"]))
+    "15. Review previous char",
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=14",
+     "SPEECH OUTPUT: 'B'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_4"))
 sequence.append(utils.AssertPresentationAction(
-    "14. Review previous word",
-    ["BRAILLE LINE:  'Open & y toggle button Quit GTK! $l'",
-     "     VISIBLE:  'Open & y toggle button Quit GTK!', cursor=29",
-     "SPEECH OUTPUT: 'GTK!'"]))
+    "16. Review previous word",
+    ["BRAILLE LINE:  '<x> Bold < > Blue $l'",
+     "     VISIBLE:  '<x> Bold < > Blue $l', cursor=10",
+     "SPEECH OUTPUT: 'not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_7"))
 sequence.append(utils.AssertPresentationAction(
-    "15. Review previous line",
-    ["BRAILLE LINE:  'File Preferences Help $l'",
-     "     VISIBLE:  'File Preferences Help $l', cursor=1",
-     "SPEECH OUTPUT: 'File Preferences Help'"]))
+    "17. Review previous line",
+    ["BRAILLE LINE:  'Shape < > Green $l'",
+     "     VISIBLE:  'Shape < > Green $l', cursor=1",
+     "SPEECH OUTPUT: 'Shape not checked Green'"]))
 
 sequence.append(utils.AssertionSummaryAction())
 sequence.start()
diff --git a/test/keystrokes/gtk-demo/role_text_multiline_flat_review.py 
b/test/keystrokes/gtk-demo/role_text_multiline_flat_review.py
index c0c74e1..8bd6c79 100644
--- a/test/keystrokes/gtk-demo/role_text_multiline_flat_review.py
+++ b/test/keystrokes/gtk-demo/role_text_multiline_flat_review.py
@@ -318,8 +318,8 @@ sequence.append(KeyReleaseAction(0, None, "KP_Insert"))
 sequence.append(utils.AssertPresentationAction(
     "22. Insert+KP_6 to flat review below",
     ["BRAILLE LINE:  'Open & y toggle button Quit GTK! $l'",
-     "     VISIBLE:  'Open & y toggle button Quit GTK!', cursor=1",
-     "SPEECH OUTPUT: 'Open'"]))
+     "     VISIBLE:  '& y toggle button Quit GTK! $l', cursor=1",
+     "SPEECH OUTPUT: 'not pressed'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyPressAction(0, None, "KP_Insert"))
diff --git a/test/keystrokes/gtk3-demo/role_dialog_flat_review.py 
b/test/keystrokes/gtk3-demo/role_dialog_flat_review.py
index 23d50db..f5d7a5b 100644
--- a/test/keystrokes/gtk3-demo/role_dialog_flat_review.py
+++ b/test/keystrokes/gtk3-demo/role_dialog_flat_review.py
@@ -280,8 +280,8 @@ sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_6"))
 sequence.append(utils.AssertPresentationAction(
     "33. Review next word",
-    ["BRAILLE LINE:  '& y Pages:  < > Reverse $l'",
-     "     VISIBLE:  '& y Pages:  < > Reverse $l', cursor=1",
+    ["BRAILLE LINE:  '& y Pages: Pages < > Reverse $l'",
+     "     VISIBLE:  '& y Pages: Pages < > Reverse $l', cursor=1",
      "SPEECH OUTPUT: 'not selected'"]))
 
 sequence.append(utils.StartRecordingAction())
diff --git a/test/keystrokes/gtk3-demo/role_menu_flat_review.py 
b/test/keystrokes/gtk3-demo/role_menu_flat_review.py
new file mode 100644
index 0000000..725cd56
--- /dev/null
+++ b/test/keystrokes/gtk3-demo/role_menu_flat_review.py
@@ -0,0 +1,226 @@
+#!/usr/bin/python
+
+"""Test of menu and menu item output."""
+
+from macaroon.playback import *
+import utils
+
+sequence = MacroSequence()
+
+sequence.append(KeyComboAction("<Control>f"))
+sequence.append(TypeAction("Application class"))
+sequence.append(KeyComboAction("Return"))
+sequence.append(KeyComboAction("Return"))
+sequence.append(KeyComboAction("<Control>r"))
+sequence.append(PauseAction(3000))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("<Alt>p"))
+sequence.append(utils.AssertPresentationAction(
+    "1. Initial menu and menu item",
+    ["BRAILLE LINE:  'gtk3-demo-application application Application Class frame Preferences menu'",
+     "     VISIBLE:  'Preferences menu', cursor=1",
+     "BRAILLE LINE:  'gtk3-demo-application application Application Class frame < > Prefer Dark Theme check 
menu item'",
+     "     VISIBLE:  '< > Prefer Dark Theme check menu', cursor=1",
+     "SPEECH OUTPUT: 'Preferences menu.'",
+     "SPEECH OUTPUT: 'Prefer Dark Theme check menu item not checked.'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_8"))
+sequence.append(utils.AssertPresentationAction(
+    "2. Review current line",
+    ["BRAILLE LINE:  '< > Prefer Dark Theme $l'",
+     "     VISIBLE:  '< > Prefer Dark Theme $l', cursor=1",
+     "SPEECH OUTPUT: 'not checked Prefer Dark Theme'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_7"))
+sequence.append(utils.AssertPresentationAction(
+    "3. Review previous line",
+    [""]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "4. Review next line",
+    ["BRAILLE LINE:  '< > Hide Titlebar when maximized $l'",
+     "     VISIBLE:  '< > Hide Titlebar when maximized', cursor=1",
+     "SPEECH OUTPUT: 'not checked Hide Titlebar when maximized'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_5"))
+sequence.append(utils.AssertPresentationAction(
+    "5. Review current word",
+    ["BRAILLE LINE:  '< > Hide Titlebar when maximized $l'",
+     "     VISIBLE:  '< > Hide Titlebar when maximized', cursor=1",
+     "SPEECH OUTPUT: 'not checked'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_6"))
+sequence.append(utils.AssertPresentationAction(
+    "6. Review next word",
+    ["BRAILLE LINE:  '< > Hide Titlebar when maximized $l'",
+     "     VISIBLE:  '< > Hide Titlebar when maximized', cursor=5",
+     "SPEECH OUTPUT: 'Hide '"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_6"))
+sequence.append(utils.AssertPresentationAction(
+    "7. Review next word",
+    ["BRAILLE LINE:  '< > Hide Titlebar when maximized $l'",
+     "     VISIBLE:  '< > Hide Titlebar when maximized', cursor=10",
+     "SPEECH OUTPUT: 'Titlebar '"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "8. Review next line",
+    ["BRAILLE LINE:  'Color $l'",
+     "     VISIBLE:  'Color $l', cursor=1",
+     "SPEECH OUTPUT: 'Color'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "9. Review next line",
+    ["BRAILLE LINE:  'Shape $l'",
+     "     VISIBLE:  'Shape $l', cursor=1",
+     "SPEECH OUTPUT: 'Shape'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "10. Review next line",
+    ["BRAILLE LINE:  '< > Bold $l'",
+     "     VISIBLE:  '< > Bold $l', cursor=1",
+     "SPEECH OUTPUT: 'not checked Bold'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "11. Review next line",
+    [""]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_5"))
+sequence.append(utils.AssertPresentationAction(
+    "12. Review current word",
+    ["BRAILLE LINE:  '< > Bold $l'",
+     "     VISIBLE:  '< > Bold $l', cursor=1",
+     "SPEECH OUTPUT: 'not checked'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_6"))
+sequence.append(utils.AssertPresentationAction(
+    "13. Review next word",
+    ["BRAILLE LINE:  '< > Bold $l'",
+     "     VISIBLE:  '< > Bold $l', cursor=5",
+     "SPEECH OUTPUT: 'Bold'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_2"))
+sequence.append(utils.AssertPresentationAction(
+    "14. Review current char",
+    ["BRAILLE LINE:  '< > Bold $l'",
+     "     VISIBLE:  '< > Bold $l', cursor=5",
+     "SPEECH OUTPUT: 'B'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_3"))
+sequence.append(utils.AssertPresentationAction(
+    "15. Review next char",
+    ["BRAILLE LINE:  '< > Bold $l'",
+     "     VISIBLE:  '< > Bold $l', cursor=6",
+     "SPEECH OUTPUT: 'o'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_1"))
+sequence.append(utils.AssertPresentationAction(
+    "16. Review previous char",
+    ["BRAILLE LINE:  '< > Bold $l'",
+     "     VISIBLE:  '< > Bold $l', cursor=5",
+     "SPEECH OUTPUT: 'B'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_1"))
+sequence.append(utils.AssertPresentationAction(
+    "17. Review previous char",
+    ["BRAILLE LINE:  '< > Bold $l'",
+     "     VISIBLE:  '< > Bold $l', cursor=1",
+     "SPEECH OUTPUT: 'not checked'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_4"))
+sequence.append(utils.AssertPresentationAction(
+    "18. Review previous word",
+    ["BRAILLE LINE:  'Shape $l'",
+     "     VISIBLE:  'Shape $l', cursor=1",
+     "SPEECH OUTPUT: 'Shape'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_4"))
+sequence.append(utils.AssertPresentationAction(
+    "19. Review previoius word",
+    ["BRAILLE LINE:  'Color $l'",
+     "     VISIBLE:  'Color $l', cursor=1",
+     "SPEECH OUTPUT: 'Color'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_Divide"))
+sequence.append(utils.AssertPresentationAction(
+    "20. Synthesize click to open menu",
+    ["KNOWN ISSUE: For some reason only a real mouse event opens this menu",
+     "BRAILLE LINE:  'gtk3-demo-application application Application Class frame Color menu'",
+     "     VISIBLE:  'Color menu', cursor=1",
+     "BRAILLE LINE:  'gtk3-demo-application application Application Class frame Color menu'",
+     "     VISIBLE:  'Color menu', cursor=1",
+     "SPEECH OUTPUT: 'Color menu.'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("Right"))
+sequence.append(utils.AssertPresentationAction(
+    "21. Right",
+    ["BRAILLE LINE:  'gtk3-demo-application application Application Class frame Preferences menu &=y Red 
radio menu item(Ctrl+R)'",
+     "     VISIBLE:  '&=y Red radio menu item(Ctrl+R)', cursor=1",
+     "SPEECH OUTPUT: 'Red selected radio menu item Ctrl+R'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_8"))
+sequence.append(utils.AssertPresentationAction(
+    "22. Review current line",
+    ["BRAILLE LINE:  '&=y Red $l'",
+     "     VISIBLE:  '&=y Red $l', cursor=1",
+     "SPEECH OUTPUT: 'selected Red'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "23. Review next line",
+    ["BRAILLE LINE:  '& y Green $l'",
+     "     VISIBLE:  '& y Green $l', cursor=1",
+     "SPEECH OUTPUT: 'not selected Green'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "24. Review next line",
+    ["BRAILLE LINE:  '& y Blue $l'",
+     "     VISIBLE:  '& y Blue $l', cursor=1",
+     "SPEECH OUTPUT: 'not selected Blue'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "25. Review next line",
+    [""]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_7"))
+sequence.append(utils.AssertPresentationAction(
+    "26. Review previous line",
+    ["BRAILLE LINE:  '& y Green $l'",
+     "     VISIBLE:  '& y Green $l', cursor=1",
+     "SPEECH OUTPUT: 'not selected Green'"]))
+
+sequence.append(utils.AssertionSummaryAction())
+sequence.start()
diff --git a/test/keystrokes/gtk3-demo/role_table_flat_review.py 
b/test/keystrokes/gtk3-demo/role_table_flat_review.py
index 66e63c8..78b05e3 100644
--- a/test/keystrokes/gtk3-demo/role_table_flat_review.py
+++ b/test/keystrokes/gtk3-demo/role_table_flat_review.py
@@ -234,14 +234,6 @@ sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_7"))
 sequence.append(utils.AssertPresentationAction(
     "28. Review previous line",
-    ["BRAILLE LINE:  '6 eggs $l'",
-     "     VISIBLE:  '6 eggs $l', cursor=1",
-     "SPEECH OUTPUT: '6 eggs'"]))
-
-sequence.append(utils.StartRecordingAction())
-sequence.append(KeyComboAction("KP_7"))
-sequence.append(utils.AssertPresentationAction(
-    "29. Review previous line",
     [""]))
 
 sequence.append(utils.AssertionSummaryAction())
diff --git a/test/keystrokes/gtk3-demo/role_text_multiline_flat_review.py 
b/test/keystrokes/gtk3-demo/role_text_multiline_flat_review.py
index da953ad..7facd2a 100644
--- a/test/keystrokes/gtk3-demo/role_text_multiline_flat_review.py
+++ b/test/keystrokes/gtk3-demo/role_text_multiline_flat_review.py
@@ -264,9 +264,9 @@ sequence.append(KeyComboAction("KP_7"))
 sequence.append(utils.AssertPresentationAction(
     "16. KP_7 to flat review toolbar",
     ["KNOWN ISSUE: gtk3-demo's toolbar widgets lack names that were present in the past",
-     "BRAILLE LINE:  'push button push button push button $l'",
-     "     VISIBLE:  'push button push button push but', cursor=1",
-     "SPEECH OUTPUT: 'push button push button push button'"]))
+     "BRAILLE LINE:  'push button & y Menu push button push button $l'",
+     "     VISIBLE:  'push button & y Menu push button', cursor=1",
+     "SPEECH OUTPUT: 'push button not pressed Menu push button push button'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_7"))
@@ -326,9 +326,9 @@ sequence.append(KeyComboAction("KP_6"))
 sequence.append(KeyReleaseAction(0, None, "KP_Insert"))
 sequence.append(utils.AssertPresentationAction(
     "23. Insert+KP_6 to flat review below",
-    ["BRAILLE LINE:  'push button push button push button $l'",
-     "     VISIBLE:  'push button push button push but', cursor=1",
-     "SPEECH OUTPUT: 'push button'"]))
+    ["BRAILLE LINE:  'push button & y Menu push button push button $l'",
+     "     VISIBLE:  'push button & y Menu push button', cursor=1",
+     "SPEECH OUTPUT: 'push '"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyPressAction(0, None, "KP_Insert"))
diff --git a/test/keystrokes/gtk3-demo/role_toggle_button_flat_review.py 
b/test/keystrokes/gtk3-demo/role_toggle_button_flat_review.py
index 723609f..b07dcb2 100644
--- a/test/keystrokes/gtk3-demo/role_toggle_button_flat_review.py
+++ b/test/keystrokes/gtk3-demo/role_toggle_button_flat_review.py
@@ -16,20 +16,30 @@ sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_8"))
 sequence.append(utils.AssertPresentationAction(
     "1. Review current line",
-    ["BRAILLE LINE:  'Something went wrong $l'",
-     "     VISIBLE:  'Something went wrong $l', cursor=1",
-     "SPEECH OUTPUT: 'Something went wrong'"]))
+    ["BRAILLE LINE:  '& y Details: $l'",
+     "     VISIBLE:  '& y Details: $l', cursor=1",
+     "SPEECH OUTPUT: 'not pressed Details:'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_7"))
 sequence.append(utils.AssertPresentationAction(
     "2. Review previous line",
-    [""]))
+    ["BRAILLE LINE:  'Here are some more details but not the full story. $l'",
+     "     VISIBLE:  'Here are some more details but n', cursor=1",
+     "SPEECH OUTPUT: 'Here are some more details but not the full story.'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_7"))
+sequence.append(utils.AssertPresentationAction(
+    "3. Review previous line",
+    ["BRAILLE LINE:  'Something went wrong $l'",
+     "     VISIBLE:  'Something went wrong $l', cursor=1",
+     "SPEECH OUTPUT: 'Something went wrong'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_9"))
 sequence.append(utils.AssertPresentationAction(
-    "3. Review next line",
+    "4. Review next line",
     ["BRAILLE LINE:  'Here are some more details but not the full story. $l'",
      "     VISIBLE:  'Here are some more details but n', cursor=1",
      "SPEECH OUTPUT: 'Here are some more details but not the full story.'"]))
@@ -37,22 +47,31 @@ sequence.append(utils.AssertPresentationAction(
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_9"))
 sequence.append(utils.AssertPresentationAction(
-    "4. Review next line",
-    ["KNOWN ISSUE: We're skipping over the Details expander",
-     "BRAILLE LINE:  'Close $l'",
+    "5. Review next line",
+    ["BRAILLE LINE:  '& y Details: $l'",
+     "     VISIBLE:  '& y Details: $l', cursor=1",
+     "SPEECH OUTPUT: 'not pressed Details:'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "6. Review next line",
+    ["BRAILLE LINE:  'Close $l'",
      "     VISIBLE:  'Close $l', cursor=1",
      "SPEECH OUTPUT: 'Close'"]))
 
 sequence.append(utils.StartRecordingAction())
-sequence.append(KeyComboAction("KP_9"))
+sequence.append(KeyComboAction("KP_7"))
 sequence.append(utils.AssertPresentationAction(
-    "5. Review next line",
-    [""]))
+    "7. Review previous line",
+    ["BRAILLE LINE:  '& y Details: $l'",
+     "     VISIBLE:  '& y Details: $l', cursor=1",
+     "SPEECH OUTPUT: 'not pressed Details:'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_7"))
 sequence.append(utils.AssertPresentationAction(
-    "6. Review previous line",
+    "8. Review previous line",
     ["BRAILLE LINE:  'Here are some more details but not the full story. $l'",
      "     VISIBLE:  'Here are some more details but n', cursor=1",
      "SPEECH OUTPUT: 'Here are some more details but not the full story.'"]))
@@ -60,7 +79,7 @@ sequence.append(utils.AssertPresentationAction(
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_6"))
 sequence.append(utils.AssertPresentationAction(
-    "7. Review next word",
+    "9. Review next word",
     ["BRAILLE LINE:  'Here are some more details but not the full story. $l'",
      "     VISIBLE:  'Here are some more details but n', cursor=6",
      "SPEECH OUTPUT: 'are '"]))
@@ -68,7 +87,7 @@ sequence.append(utils.AssertPresentationAction(
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_6"))
 sequence.append(utils.AssertPresentationAction(
-    "8. Review next word",
+    "10. Review next word",
     ["BRAILLE LINE:  'Here are some more details but not the full story. $l'",
      "     VISIBLE:  'Here are some more details but n', cursor=10",
      "SPEECH OUTPUT: 'some '"]))
@@ -76,7 +95,7 @@ sequence.append(utils.AssertPresentationAction(
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_6"))
 sequence.append(utils.AssertPresentationAction(
-    "9. Review next word",
+    "11. Review next word",
     ["BRAILLE LINE:  'Here are some more details but not the full story. $l'",
      "     VISIBLE:  'Here are some more details but n', cursor=15",
      "SPEECH OUTPUT: 'more '"]))
@@ -84,7 +103,7 @@ sequence.append(utils.AssertPresentationAction(
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_6"))
 sequence.append(utils.AssertPresentationAction(
-    "10. Review next word",
+    "12. Review next word",
     ["BRAILLE LINE:  'Here are some more details but not the full story. $l'",
      "     VISIBLE:  'Here are some more details but n', cursor=20",
      "SPEECH OUTPUT: 'details '"]))
@@ -92,7 +111,7 @@ sequence.append(utils.AssertPresentationAction(
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_6"))
 sequence.append(utils.AssertPresentationAction(
-    "11. Review next word",
+    "13. Review next word",
     ["BRAILLE LINE:  'Here are some more details but not the full story. $l'",
      "     VISIBLE:  'Here are some more details but n', cursor=28",
      "SPEECH OUTPUT: 'but '"]))
@@ -100,7 +119,7 @@ sequence.append(utils.AssertPresentationAction(
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_6"))
 sequence.append(utils.AssertPresentationAction(
-    "12. Review next word",
+    "14. Review next word",
     ["BRAILLE LINE:  'Here are some more details but not the full story. $l'",
      "     VISIBLE:  'Here are some more details but n', cursor=32",
      "SPEECH OUTPUT: 'not '"]))
@@ -108,7 +127,7 @@ sequence.append(utils.AssertPresentationAction(
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_6"))
 sequence.append(utils.AssertPresentationAction(
-    "13. Review next word",
+    "15. Review next word",
     ["BRAILLE LINE:  'Here are some more details but not the full story. $l'",
      "     VISIBLE:  'ot the full story. $l', cursor=4",
      "SPEECH OUTPUT: 'the '"]))
@@ -116,7 +135,7 @@ sequence.append(utils.AssertPresentationAction(
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_6"))
 sequence.append(utils.AssertPresentationAction(
-    "14. Review next word",
+    "16. Review next word",
     ["BRAILLE LINE:  'Here are some more details but not the full story. $l'",
      "     VISIBLE:  'ot the full story. $l', cursor=8",
      "SPEECH OUTPUT: 'full '"]))
@@ -124,24 +143,15 @@ sequence.append(utils.AssertPresentationAction(
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_6"))
 sequence.append(utils.AssertPresentationAction(
-    "15. Review next word",
+    "17. Review next word",
     ["BRAILLE LINE:  'Here are some more details but not the full story. $l'",
      "     VISIBLE:  'ot the full story. $l', cursor=13",
      "SPEECH OUTPUT: 'story.'"]))
 
 sequence.append(utils.StartRecordingAction())
-sequence.append(KeyComboAction("KP_6"))
-sequence.append(utils.AssertPresentationAction(
-    "16. Review next word",
-    ["KNOWN ISSUE: We're skipping over the Details expander",
-     "BRAILLE LINE:  'Close $l'",
-     "     VISIBLE:  'Close $l', cursor=1",
-     "SPEECH OUTPUT: 'Close'"]))
-
-sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Tab"))
 sequence.append(utils.AssertPresentationAction(
-    "17. Tab to change focus",
+    "18. Tab to change focus",
     ["KNOWN ISSUE: This is not on screen. But we don't control focus.",
      "BRAILLE LINE:  'already ! $l rdonly'",
      "     VISIBLE:  'already ! $l rdonly', cursor=10",
@@ -153,7 +163,7 @@ sequence.append(utils.AssertPresentationAction(
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("KP_8"))
 sequence.append(utils.AssertPresentationAction(
-    "18. Review current line",
+    "19. Review current line",
     ["BRAILLE LINE:  'Something went wrong $l'",
      "     VISIBLE:  'Something went wrong $l', cursor=1",
      "SPEECH OUTPUT: 'Something went wrong'"]))
diff --git a/test/keystrokes/oowriter/ui_role_menu_flat_review.py 
b/test/keystrokes/oowriter/ui_role_menu_flat_review.py
new file mode 100644
index 0000000..062ecb3
--- /dev/null
+++ b/test/keystrokes/oowriter/ui_role_menu_flat_review.py
@@ -0,0 +1,160 @@
+#!/usr/bin/python
+
+"""Test of menu and menu item output."""
+
+from macaroon.playback import *
+import utils
+
+sequence = MacroSequence()
+
+sequence.append(PauseAction(3000))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("<Alt>v"))
+sequence.append(utils.AssertPresentationAction(
+    "1. Initial menu and menu item",
+    ["BRAILLE LINE:  'soffice application Untitled 1 - LibreOffice Writer frame View menu'",
+     "     VISIBLE:  'View menu', cursor=1",
+     "BRAILLE LINE:  'soffice application Untitled 1 - LibreOffice Writer frame &=y Normal radio menu item'",
+     "     VISIBLE:  '&=y Normal radio menu item', cursor=1",
+     "SPEECH OUTPUT: 'View menu.'",
+     "SPEECH OUTPUT: 'Normal selected radio menu item'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("Down"))
+sequence.append(utils.AssertPresentationAction(
+    "2. Down",
+    ["BRAILLE LINE:  'soffice application Untitled 1 - LibreOffice Writer frame & y Web radio menu item'",
+     "     VISIBLE:  '& y Web radio menu item', cursor=1",
+     "SPEECH OUTPUT: 'Web not selected radio menu item'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_8"))
+sequence.append(utils.AssertPresentationAction(
+    "3. Review currernt line",
+    ["BRAILLE LINE:  '& y Web $l'",
+     "     VISIBLE:  '& y Web $l', cursor=1",
+     "SPEECH OUTPUT: 'not selected Web'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "4. Review next line",
+    ["BRAILLE LINE:  'separator $l'",
+     "     VISIBLE:  'separator $l', cursor=1",
+     "SPEECH OUTPUT: 'separator'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "5. Review next line",
+    ["BRAILLE LINE:  'Toolbars $l'",
+     "     VISIBLE:  'Toolbars $l', cursor=1",
+     "SPEECH OUTPUT: 'Toolbars'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "6. Review next line",
+    ["BRAILLE LINE:  '<x> Status Bar $l'",
+     "     VISIBLE:  '<x> Status Bar $l', cursor=1",
+     "SPEECH OUTPUT: 'checked Status Bar'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_9"))
+sequence.append(utils.AssertPresentationAction(
+    "7. Review next line",
+    ["BRAILLE LINE:  'Rulers $l'",
+     "     VISIBLE:  'Rulers $l', cursor=1",
+     "SPEECH OUTPUT: 'Rulers'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_5"))
+sequence.append(utils.AssertPresentationAction(
+    "8. Review current word",
+    ["BRAILLE LINE:  'Rulers $l'",
+     "     VISIBLE:  'Rulers $l', cursor=1",
+     "SPEECH OUTPUT: 'Rulers'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_6"))
+sequence.append(utils.AssertPresentationAction(
+    "9. Review next word",
+    ["BRAILLE LINE:  'Scrollbars $l'",
+     "     VISIBLE:  'Scrollbars $l', cursor=1",
+     "SPEECH OUTPUT: 'Scrollbars'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_2"))
+sequence.append(utils.AssertPresentationAction(
+    "10. Review current char",
+    ["BRAILLE LINE:  'Scrollbars $l'",
+     "     VISIBLE:  'Scrollbars $l', cursor=1",
+     "SPEECH OUTPUT: 'S'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_3"))
+sequence.append(utils.AssertPresentationAction(
+    "11. Review next char",
+    ["BRAILLE LINE:  'Scrollbars $l'",
+     "     VISIBLE:  'Scrollbars $l', cursor=2",
+     "SPEECH OUTPUT: 'c'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_6"))
+sequence.append(utils.AssertPresentationAction(
+    "12. Review next word",
+    ["BRAILLE LINE:  'separator $l'",
+     "     VISIBLE:  'separator $l', cursor=1",
+     "SPEECH OUTPUT: 'separator'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_2"))
+sequence.append(utils.AssertPresentationAction(
+    "13. Review current char",
+    ["BRAILLE LINE:  'separator $l'",
+     "     VISIBLE:  'separator $l', cursor=1",
+     "SPEECH OUTPUT: 'separator'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_3"))
+sequence.append(utils.AssertPresentationAction(
+    "14. Review next char",
+    ["BRAILLE LINE:  '<x> Text Boundaries $l'",
+     "     VISIBLE:  '<x> Text Boundaries $l', cursor=1",
+     "SPEECH OUTPUT: 'checked'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_1"))
+sequence.append(utils.AssertPresentationAction(
+    "15. Review previous char",
+    ["BRAILLE LINE:  'separator $l'",
+     "     VISIBLE:  'separator $l', cursor=1",
+     "SPEECH OUTPUT: 'separator'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_1"))
+sequence.append(utils.AssertPresentationAction(
+    "16. Review previous char",
+    ["BRAILLE LINE:  'Scrollbars $l'",
+     "     VISIBLE:  'Scrollbars $l', cursor=10",
+     "SPEECH OUTPUT: 's'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_4"))
+sequence.append(utils.AssertPresentationAction(
+    "17. Review previous word",
+    ["BRAILLE LINE:  'Rulers $l'",
+     "     VISIBLE:  'Rulers $l', cursor=1",
+     "SPEECH OUTPUT: 'Rulers'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("KP_7"))
+sequence.append(utils.AssertPresentationAction(
+    "18. Review previous line",
+    ["BRAILLE LINE:  '<x> Status Bar $l'",
+     "     VISIBLE:  '<x> Status Bar $l', cursor=1",
+     "SPEECH OUTPUT: 'checked Status Bar'"]))
+
+sequence.append(utils.AssertionSummaryAction())
+sequence.start()


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