Index: test/keystrokes/firefox/line_nav_bugzilla_search.py
===================================================================
--- test/keystrokes/firefox/line_nav_bugzilla_search.py	(revision 4184)
+++ test/keystrokes/firefox/line_nav_bugzilla_search.py	(working copy)
@@ -299,7 +299,7 @@
     "Line Down",
     ["BRAILLE LINE:  '<x> CheckBox the bug assignee'",
      "     VISIBLE:  '<x> CheckBox the bug assignee', cursor=1",
-     "SPEECH OUTPUT: 'the bug assignee check box checked the bug assignee'"]))
+     "SPEECH OUTPUT: 'the bug assignee check box checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Down"))
@@ -307,7 +307,7 @@
     "Line Down",
     ["BRAILLE LINE:  '< > CheckBox the reporter'",
      "     VISIBLE:  '< > CheckBox the reporter', cursor=1",
-     "SPEECH OUTPUT: 'the reporter check box not checked the reporter'"]))
+     "SPEECH OUTPUT: 'the reporter check box not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Down"))
@@ -315,7 +315,7 @@
     "Line Down",
     ["BRAILLE LINE:  '< > CheckBox the QA contact'",
      "     VISIBLE:  '< > CheckBox the QA contact', cursor=1",
-     "SPEECH OUTPUT: 'the QA contact check box not checked the QA contact'"]))
+     "SPEECH OUTPUT: 'the QA contact check box not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Down"))
@@ -323,7 +323,7 @@
     "Line Down",
     ["BRAILLE LINE:  '< > CheckBox a CC list member'",
      "     VISIBLE:  '< > CheckBox a CC list member', cursor=1",
-     "SPEECH OUTPUT: 'a CC list member check box not checked a CC list member'"]))
+     "SPEECH OUTPUT: 'a CC list member check box not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Down"))
@@ -331,7 +331,7 @@
     "Line Down",
     ["BRAILLE LINE:  '< > CheckBox a commenter'",
      "     VISIBLE:  '< > CheckBox a commenter', cursor=1",
-     "SPEECH OUTPUT: 'a commenter check box not checked a commenter'"]))
+     "SPEECH OUTPUT: 'a commenter check box not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Down"))
@@ -363,7 +363,7 @@
     "Line Down",
     ["BRAILLE LINE:  '<x> CheckBox the bug assignee'",
      "     VISIBLE:  '<x> CheckBox the bug assignee', cursor=1",
-     "SPEECH OUTPUT: 'the bug assignee check box checked the bug assignee'"]))
+     "SPEECH OUTPUT: 'the bug assignee check box checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Down"))
@@ -371,7 +371,7 @@
     "Line Down",
     ["BRAILLE LINE:  '<x> CheckBox the reporter'",
      "     VISIBLE:  '<x> CheckBox the reporter', cursor=1",
-     "SPEECH OUTPUT: 'the reporter check box checked the reporter'"]))
+     "SPEECH OUTPUT: 'the reporter check box checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Down"))
@@ -379,7 +379,7 @@
     "Line Down",
     ["BRAILLE LINE:  '<x> CheckBox the QA contact'",
      "     VISIBLE:  '<x> CheckBox the QA contact', cursor=1",
-     "SPEECH OUTPUT: 'the QA contact check box checked the QA contact'"]))
+     "SPEECH OUTPUT: 'the QA contact check box checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Down"))
@@ -387,7 +387,7 @@
     "Line Down",
     ["BRAILLE LINE:  '<x> CheckBox a CC list member'",
      "     VISIBLE:  '<x> CheckBox a CC list member', cursor=1",
-     "SPEECH OUTPUT: 'a CC list member check box checked a CC list member'"]))
+     "SPEECH OUTPUT: 'a CC list member check box checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Down"))
@@ -395,7 +395,7 @@
     "Line Down",
     ["BRAILLE LINE:  '< > CheckBox a commenter'",
      "     VISIBLE:  '< > CheckBox a commenter', cursor=1",
-     "SPEECH OUTPUT: 'a commenter check box not checked a commenter'"]))
+     "SPEECH OUTPUT: 'a commenter check box not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Down"))
@@ -580,7 +580,7 @@
     "Line Down",
     ["BRAILLE LINE:  '< > CheckBox Not (negate this whole chart)'",
      "     VISIBLE:  '< > CheckBox Not (negate this wh', cursor=1",
-     "SPEECH OUTPUT: 'Not (negate this whole chart) check box not checked Not (negate this whole chart)'"]))
+     "SPEECH OUTPUT: 'Not (negate this whole chart) check box not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Down"))
@@ -647,7 +647,7 @@
     "Line Up",
     ["BRAILLE LINE:  '< > CheckBox Not (negate this whole chart)'",
      "     VISIBLE:  '< > CheckBox Not (negate this wh', cursor=1",
-     "SPEECH OUTPUT: 'Not (negate this whole chart) check box not checked Not (negate this whole chart)'"]))
+     "SPEECH OUTPUT: 'Not (negate this whole chart) check box not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Up"))
@@ -832,7 +832,7 @@
     "Line Up",
     ["BRAILLE LINE:  '< > CheckBox a commenter'",
      "     VISIBLE:  '< > CheckBox a commenter', cursor=1",
-     "SPEECH OUTPUT: 'a commenter check box not checked a commenter'"]))
+     "SPEECH OUTPUT: 'a commenter check box not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Up"))
@@ -840,7 +840,7 @@
     "Line Up",
     ["BRAILLE LINE:  '<x> CheckBox a CC list member'",
      "     VISIBLE:  '<x> CheckBox a CC list member', cursor=1",
-     "SPEECH OUTPUT: 'a CC list member check box checked a CC list member'"]))
+     "SPEECH OUTPUT: 'a CC list member check box checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Up"))
@@ -848,7 +848,7 @@
     "Line Up",
     ["BRAILLE LINE:  '<x> CheckBox the QA contact'",
      "     VISIBLE:  '<x> CheckBox the QA contact', cursor=1",
-     "SPEECH OUTPUT: 'the QA contact check box checked the QA contact'"]))
+     "SPEECH OUTPUT: 'the QA contact check box checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Up"))
@@ -856,7 +856,7 @@
     "Line Up",
     ["BRAILLE LINE:  '<x> CheckBox the reporter'",
      "     VISIBLE:  '<x> CheckBox the reporter', cursor=1",
-     "SPEECH OUTPUT: 'the reporter check box checked the reporter'"]))
+     "SPEECH OUTPUT: 'the reporter check box checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Up"))
@@ -864,7 +864,7 @@
     "Line Up",
     ["BRAILLE LINE:  '<x> CheckBox the bug assignee'",
      "     VISIBLE:  '<x> CheckBox the bug assignee', cursor=1",
-     "SPEECH OUTPUT: 'the bug assignee check box checked the bug assignee'"]))
+     "SPEECH OUTPUT: 'the bug assignee check box checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Up"))
@@ -896,7 +896,7 @@
     "Line Up",
     ["BRAILLE LINE:  '< > CheckBox a commenter'",
      "     VISIBLE:  '< > CheckBox a commenter', cursor=1",
-     "SPEECH OUTPUT: 'a commenter check box not checked a commenter'"]))
+     "SPEECH OUTPUT: 'a commenter check box not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Up"))
@@ -904,7 +904,7 @@
     "Line Up",
     ["BRAILLE LINE:  '< > CheckBox a CC list member'",
      "     VISIBLE:  '< > CheckBox a CC list member', cursor=1",
-     "SPEECH OUTPUT: 'a CC list member check box not checked a CC list member'"]))
+     "SPEECH OUTPUT: 'a CC list member check box not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Up"))
@@ -912,7 +912,7 @@
     "Line Up",
     ["BRAILLE LINE:  '< > CheckBox the QA contact'",
      "     VISIBLE:  '< > CheckBox the QA contact', cursor=1",
-     "SPEECH OUTPUT: 'the QA contact check box not checked the QA contact'"]))
+     "SPEECH OUTPUT: 'the QA contact check box not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Up"))
@@ -920,7 +920,7 @@
     "Line Up",
     ["BRAILLE LINE:  '< > CheckBox the reporter'",
      "     VISIBLE:  '< > CheckBox the reporter', cursor=1",
-     "SPEECH OUTPUT: 'the reporter check box not checked the reporter'"]))
+     "SPEECH OUTPUT: 'the reporter check box not checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Up"))
@@ -928,7 +928,7 @@
     "Line Up",
     ["BRAILLE LINE:  '<x> CheckBox the bug assignee'",
      "     VISIBLE:  '<x> CheckBox the bug assignee', cursor=1",
-     "SPEECH OUTPUT: 'the bug assignee check box checked the bug assignee'"]))
+     "SPEECH OUTPUT: 'the bug assignee check box checked'"]))
 
 sequence.append(utils.StartRecordingAction())
 sequence.append(KeyComboAction("Up"))
Index: src/orca/scripts/toolkits/Gecko/script.py
===================================================================
--- src/orca/scripts/toolkits/Gecko/script.py	(revision 4184)
+++ src/orca/scripts/toolkits/Gecko/script.py	(working copy)
@@ -179,6 +179,13 @@
         self.currentLineContents = None
         self._nextLineContents = None
 
+        # guessTheLabel() is an expensive method. If we cache the guessed
+        # labels, we'll see a performance improvement when a form field
+        # is Tab/Shift+Tab'ed/Arrowed back to. In addition, we can check
+        # for non-label labels when looking at line content.
+        #
+        self._guessedLabels = {}
+
         # For really large objects, a call to getAttributes can take up to
         # two seconds! This is a Firefox bug. We'll try to improve things
         # by storing attributes.
@@ -1338,6 +1345,13 @@
         """
 
         self._destroyLineCache()
+
+        # If text is removed from something which is not editable, trash our
+        # saved guessed labels to be on the safe side.
+        #
+        if not event.source.getState().contains(pyatspi.STATE_EDITABLE):
+            self._guessedLabels = {}
+
         default.Script.onTextDeleted(self, event)
 
     def onTextInserted(self, event):
@@ -1348,6 +1362,12 @@
         """
         self._destroyLineCache()
 
+        # If text is inserted into something which is not editable, trash our
+        # saved guessed labels to be on the safe side.
+        #
+        if not event.source.getState().contains(pyatspi.STATE_EDITABLE):
+            self._guessedLabels = {}
+
         # handle live region events
         if self.handleAsLiveRegion(event):
             self.liveMngr.handleEvent(event)
@@ -1360,6 +1380,12 @@
         for addition events often associated with Javascipt insertion.  One such
         such example would be the programmatic insertion of a tooltip or alert
         dialog."""
+
+        # If children are being added or removed, trash our saved guessed
+        # labels to be on the safe side.
+        #
+        self._guessedLabels = {}
+
         # no need moving forward if we don't have our target.
         if event.any_data is None:
             return
@@ -1591,6 +1617,11 @@
         if event.type.startswith("object:state-changed:busy"):
             if event.source \
                 and (event.source.getRole() == pyatspi.ROLE_DOCUMENT_FRAME):
+
+                # If content is changing, trash our saved guessed labels.
+                #
+                self._guessedLabels = {}
+
                 finishedLoading = False
                 if orca_state.locusOfFocus \
                     and (orca_state.locusOfFocus.getRole() \
@@ -1804,6 +1835,16 @@
                   self.findFirstCaretContext(newLocusOfFocus, caretOffset)
             self.setCaretContext(obj, characterOffset)
 
+        else:
+            # If the newLocusOfFocus is not in document content, trash
+            # our stored guessed labels. This will hopefully maximize
+            # performance and accuracy when navigating amongst form 
+            # fields while minimizing the cache size (Gecko creates and
+            # destroys accessibles so frequently that hashing is of no
+            # use).
+            #
+            self._guessedLabels = {}
+
         # If we've just landed in the Find toolbar, reset
         # self.madeFindAnnouncement.
         #
@@ -4101,6 +4142,12 @@
            or self.isAriaWidget(obj):
             return guess
 
+        # Maybe we've already made a guess and saved it.
+        #
+        for field, label in self._guessedLabels.items():
+            if self.isSameObject(field, obj):
+                return label
+
         # Because the guesswork is based upon spatial relations, if we're
         # in a list, look from the perspective of the first list item rather
         # than from the list as a whole.
@@ -4130,6 +4177,12 @@
             guess = obj.name
             #print "Guessing the name: ", guess
 
+        if obj.parent.getRole() == pyatspi.ROLE_LIST:
+            obj = obj.parent
+
+        guess = guess.strip()
+        self._guessedLabels[obj] = guess
+
         return guess.strip()
 
     ####################################################################
@@ -5045,6 +5098,7 @@
         doNotSpeakRoles = [pyatspi.ROLE_DOCUMENT_FRAME, pyatspi.ROLE_HEADING]
 
         utterances = []
+        prevObj = None
         for content in contents:
             [obj, startOffset, endOffset, string] = content
             role = obj.getRole()
@@ -5065,13 +5119,28 @@
             elif role == pyatspi.ROLE_IMAGE and self.isUselessObject(obj):
                 continue
 
+            # If the focused item is a checkbox or a radio button for which
+            # we had to guess the label, odds are that the guessed label is
+            # immediately to the right. Under these circumstances, we'll
+            # double speak the "label". It would be nice to avoid that.
+            # [[[TODO - JD: This is the simple version. It does not handle
+            # the possibility of the fake label being comprised of multiple
+            # objects.]]]
+            #
+            if prevObj \
+               and prevObj.getRole() in [pyatspi.ROLE_CHECK_BOX,
+                                         pyatspi.ROLE_RADIO_BUTTON] \
+               and prevObj.getState().contains(pyatspi.STATE_FOCUSED):
+                if self.guessTheLabel(prevObj) == string.strip():
+                    continue
+
             # The radio button's label gets added to the context in
             # default.locusOfFocusChanged() and not through the speech
             # generator -- unless we wind up having to guess the label.
             # Therefore, if we have a valid label for a radio button,
             # we need to add it here.
             #
-            if (obj.getRole() == pyatspi.ROLE_RADIO_BUTTON) \
+            if (role == pyatspi.ROLE_RADIO_BUTTON) \
                 and not self.isAriaWidget(obj):
                 label = self.getDisplayedLabel(obj)
                 if label:
@@ -5110,6 +5179,8 @@
             for item in utterance:
                 utterances.append([item, self.getACSS(obj, item)])
   
+            prevObj = obj
+
         return utterances
 
     def clumpUtterances(self, utterances):
