Index: src/orca/Gecko.py
===================================================================
--- src/orca/Gecko.py	(revision 2273)
+++ src/orca/Gecko.py	(working copy)
@@ -82,6 +82,17 @@
                 rolenames.ROLE_TEXT,
                 rolenames.ROLE_AUTOCOMPLETE]
 
+# Roles that represent fields in a form
+#
+FORM_ROLES = [rolenames.ROLE_CHECK_BOX,
+              rolenames.ROLE_COMBO_BOX,
+              rolenames.ROLE_ENTRY,
+              rolenames.ROLE_LIST,
+              rolenames.ROLE_PASSWORD_TEXT,
+              rolenames.ROLE_PUSH_BUTTON,
+              rolenames.ROLE_RADIO_BUTTON,
+              rolenames.ROLE_SPIN_BUTTON]
+
 ########################################################################
 #                                                                      #
 # Custom BrailleGenerator                                              #
@@ -285,6 +296,49 @@
         #
         return [regions, regions[focusedRegionIndex]]
 
+    def _getBrailleRegionsForList(self, obj):
+        """Get the braille for a list in a form.  If the list already has
+        focus, then only the selection is displayed.
+
+        Arguments:
+        - obj: the list
+
+        Returns a list where the first element is a list of Regions to display
+        and the second element is the Region which should get focus.
+        """
+
+        if not obj.state.count(atspi.Accessibility.STATE_FOCUSABLE):
+            bg = braillegenerator.BrailleGenerator
+            return bg._getBrailleRegionsForList(self, obj, already_focused)
+
+        self._debugGenerator("Gecko._getBrailleRegionsForList", obj)
+
+        regions = []
+
+        label = self._script.getDisplayedLabel(obj)
+        if not label:
+            label = obj.name
+
+        focusedRegionIndex = 0
+        if label and len(label):
+            regions.append(braille.Region(label + " "))
+            focusedRegionIndex = 1
+
+        item = None
+        for i in range(0, obj.childCount):
+            if obj.selection.isChildSelected(i):
+                item = obj.child(i)
+                break
+        if not item:
+            item = obj.child(0)
+        regions.append(braille.Region(item.name))
+
+        if settings.brailleVerbosityLevel == settings.VERBOSITY_LEVEL_VERBOSE:
+            regions.append(braille.Region(
+                " " + rolenames.getBrailleForRoleName(obj)))
+
+        return [regions, regions[focusedRegionIndex]]
+
     def _getBrailleRegionsForImage(self, obj):
         """Get the braille regions for an image.
 
@@ -480,9 +534,6 @@
         if not already_focused and label:
             utterances.append(label)
 
-        if not already_focused:
-            utterances.extend(self._getSpeechForObjectRole(obj))
-
         menu = None
         for i in range(0, obj.childCount):
             child = obj.child(i)
@@ -513,6 +564,9 @@
 
         utterances.extend(self._getSpeechForObjectAvailability(obj))
 
+        if not already_focused:
+            utterances.extend(self._getSpeechForObjectRole(obj))
+
         self._debugGenerator("Gecko._getSpeechForComboBox",
                              obj,
                              already_focused,
@@ -520,6 +574,80 @@
 
         return utterances
 
+    def _getSpeechForMenuItem(self, obj, already_focused):
+        """Get the speech for a menu item.
+
+        Arguments:
+        - obj: the menu item
+        - already_focused: False if object just received focus
+
+        Returns a list of utterances to be spoken for the object.
+        """
+
+        if obj.parent.role != rolenames.ROLE_LIST:
+            sg = speechgenerator.SpeechGenerator
+            return sg._getSpeechForMenuItem(self, obj, already_focused)
+
+        # No need to say "menu item" because we already know that.
+        #
+        utterances = self._getSpeechForObjectName(obj)
+
+        self._debugGenerator("Gecko._getSpeechForMenuItem",
+                             obj,
+                             already_focused,
+                             utterances)
+
+        return utterances
+
+    def _getSpeechForList(self, obj, already_focused):
+        """Get the speech for a list.
+
+        Arguments:
+        - obj: the list
+        - already_focused: False if object just received focus
+
+        Returns a list of utterances to be spoken for the object.
+        """
+
+        if not obj.state.count(atspi.Accessibility.STATE_FOCUSABLE):
+            sg = speechgenerator.SpeechGenerator
+            return sg._getSpeechForList(self, obj, already_focused)
+
+        utterances = []
+
+        label = self._script.getDisplayedLabel(obj)
+        if not label:
+            label = obj.name
+
+        if not already_focused and label:
+            utterances.append(label)
+
+        item = None
+        for i in range(0, obj.childCount):
+            if obj.selection.isChildSelected(i):
+                item = obj.child(i)
+                break
+        if i == obj.childCount - 1:
+            item = obj.child(0)
+        if item:
+            utterances.extend(
+                      self._getSpeechForMenuItem(item, already_focused))
+
+        if not already_focused:
+            if obj.state.count(atspi.Accessibility.STATE_MULTISELECTABLE):
+                # Translators: "multi-select" refers to a web form list
+                # in which more than one item can be selected at a time.
+                #
+                utterances.append(_("multi-select"))
+            utterances.extend(self._getSpeechForObjectRole(obj))
+
+        self._debugGenerator("Gecko._getSpeechForList",
+                             obj,
+                             already_focused,
+                             utterances)
+
+        return utterances
+
     def _getSpeechForImage(self, obj, already_focused):
         """Gets a list of utterances to be spoken for an image.
 
@@ -756,6 +884,8 @@
              Script.goPreviousUnvisitedLink,
              Script.goNextVisitedLink,
              Script.goPreviousVisitedLink,
+             Script.goNextFormField,
+             Script.goPreviousFormField,
              Script.goNextTable,
              Script.goPreviousTable,
              Script.goCellLeft,
@@ -1088,6 +1218,22 @@
                 #
                 _("Goes to next visited link."))
 
+        self.inputEventHandlers["goPreviousFormFieldHandler"] = \
+            input_event.InputEventHandler(
+                Script.goPreviousFormField,
+                # Translators: this is for navigating between form fields in
+                # HTML
+                #
+                _("Goes to previous form field."))
+
+        self.inputEventHandlers["goNextFormFieldHandler"] = \
+            input_event.InputEventHandler(
+                Script.goNextFormField,
+                # Translators: this is for navigating between form fields in
+                # HTML
+                #
+                _("Goes to next form field."))
+
         self.inputEventHandlers["goPreviousTableHandler"] = \
             input_event.InputEventHandler(
                 Script.goPreviousTable,
@@ -1489,6 +1635,27 @@
 
         keyBindings.add(
             keybindings.KeyBinding(
+                "Tab",
+                (1 << settings.MODIFIER_ORCA \
+                 | 1 << atspi.Accessibility.MODIFIER_SHIFT \
+                 | 1 << atspi.Accessibility.MODIFIER_ALT \
+                 | 1 << atspi.Accessibility.MODIFIER_CONTROL),
+                (1 << settings.MODIFIER_ORCA | \
+                     1 << atspi.Accessibility.MODIFIER_SHIFT),
+                self.inputEventHandlers["goPreviousFormFieldHandler"]))
+
+        keyBindings.add(
+            keybindings.KeyBinding(
+                "Tab",
+                (1 << settings.MODIFIER_ORCA \
+                 | 1 << atspi.Accessibility.MODIFIER_SHIFT \
+                 | 1 << atspi.Accessibility.MODIFIER_ALT \
+                 | 1 << atspi.Accessibility.MODIFIER_CONTROL),
+                1 << settings.MODIFIER_ORCA,
+                self.inputEventHandlers["goNextFormFieldHandler"]))
+
+        keyBindings.add(
+            keybindings.KeyBinding(
                 "t",
                 (1 << atspi.Accessibility.MODIFIER_SHIFT \
                  | 1 << atspi.Accessibility.MODIFIER_ALT \
@@ -2541,15 +2708,23 @@
             weHandleIt = keyboardEvent.event_string in ["Left", "Right"]
 
         elif obj and (obj.role == rolenames.ROLE_MENU_ITEM):
-            # We'll let Firefox handle the navigation of combo boxes.
+            # We'll let Firefox handle the navigation of combo boxes and
+            # lists.
             #
             parent = obj.parent
             if parent:
-                parent = parent.parent
-                if parent and (parent.role == rolenames.ROLE_COMBO_BOX):
+                if parent.role == rolenames.ROLE_LIST or \
+                   (parent.parent and \
+                    parent.parent.role == rolenames.ROLE_COMBO_BOX):
                     weHandleIt = \
                         keyboardEvent.event_string in ["Left", "Right"]
 
+        elif obj and (obj.role == rolenames.ROLE_LIST):
+            # We'll let Firefox handle the navigation of lists in forms.
+            #
+            if obj.state.count(atspi.Accessibility.STATE_FOCUSABLE):
+                weHandleIt = False
+
         return weHandleIt
 
     def useStructuralNavigationModel(self):
@@ -2563,6 +2738,14 @@
                             rolenames.ROLE_TEXT,
                             rolenames.ROLE_PASSWORD_TEXT]
 
+        # If the Orca_Modifier key was pressed, we're handling it.
+        #
+        if isinstance(orca_state.lastInputEvent, input_event.KeyboardEvent):
+            mods = orca_state.lastInputEvent.modifiers
+            isOrcaKey = mods & (1 << settings.MODIFIER_ORCA)
+            if isOrcaKey:
+                return True
+
         obj = orca_state.locusOfFocus
         while obj:
             if obj.role == rolenames.ROLE_DOCUMENT_FRAME:
@@ -4459,6 +4642,156 @@
             #
             speech.speak(_("No more visited links."))
 
+    def goPreviousFormField(self, inputEvent):
+        found = False
+        [obj, characterOffset] = self.getCaretContext()
+        while obj and not found:
+            obj = self.findPreviousRole(FORM_ROLES, obj)
+            if obj:
+                found = (obj.role != rolenames.ROLE_LIST_ITEM) or \
+                        (obj.role == rolenames.ROLE_LIST and \
+                         obj.state.count(atspi.Accessibility.STATE_FOCUSABLE))
+
+        if obj:
+            speakContents = True
+            if obj.role in [rolenames.ROLE_LIST,
+                            rolenames.ROLE_COMBO_BOX]:
+                # We need to do some explicit focus-grabbing so that Firefox
+                # knows exactly where we are.  If we do this, we do not want
+                # to speak the object contents.
+                #
+                focusGrabbed = obj.component.grabFocus()
+                orca.setLocusOfFocus(None, obj, False)
+                speakContents = False
+
+                if obj.role == rolenames.ROLE_COMBO_BOX:
+                    obj = obj.child(0)
+
+                if obj.selection:
+                    for i in range(0, obj.childCount):
+                        if obj.selection.isChildSelected(i):
+                            obj = obj.child(i)
+                            break
+                    if i == obj.childCount - 1:
+                        obj = obj.child(0)
+
+            [obj, characterOffset] = self.findFirstCaretContext(obj, 0)
+            if obj and obj.role in [rolenames.ROLE_CHECK_BOX,
+                                    rolenames.ROLE_RADIO_BUTTON]:
+                # [[[TODO: HACK - JD.  Explicit focus-grabbing doesn't
+                # work on all objects.  But form fields by their very
+                # nature have text around them.  Therefore, let's try 
+                # quietly updating our caretContext to the text that's
+                # just before the form field we care about before setting
+                # the caret position to that form field.]]]
+                #
+                [prevObject, prevOffset] = \
+                            self.findPreviousCaretInOrder(obj, -1, False)
+                if prevObject:
+                    self.setCaretPosition(prevObject, prevOffset)
+
+            elif obj and obj.role in [rolenames.ROLE_ENTRY,
+                                      rolenames.ROLE_PASSWORD_TEXT]:
+                # [[[TODO: HACK - JD.  If it's an entry, try to activate it
+                # through the AccessibleAction interface.  NOTE: This seems
+                # to work fine for single-line entries and fail for multi-
+                # line ones. ]]]
+                #
+                name = None
+                if obj.action:
+                    for i in range(0, obj.action.nActions):
+                        name = obj.action.getName(i)
+                        if name == "activate":
+                            break
+                    if name:
+                         success = obj.action.doAction(i)
+
+            self.setCaretPosition(obj, characterOffset)
+            self.updateBraille(obj)
+            if speakContents:
+                self.speakContents(self.getObjectContentsAtOffset(obj,
+                                                             characterOffset))
+        else:
+            # Translators: this is for navigating HTML content by
+            # moving from form field to form field.
+            #
+            speech.speak(_("No more form fields."))
+
+    def goNextFormField(self, inputEvent):
+        found = False
+        [obj, characterOffset] = self.getCaretContext()
+        while obj and not found:
+            obj = self.findNextRole(FORM_ROLES, obj)
+            if obj:
+                found = (obj.role != rolenames.ROLE_LIST_ITEM) or \
+                        (obj.role == rolenames.ROLE_LIST and \
+                         obj.state.count(atspi.Accessibility.STATE_FOCUSABLE))
+
+        if obj:
+            speakContents = True
+            if obj.role in [rolenames.ROLE_LIST,
+                            rolenames.ROLE_COMBO_BOX]:
+                # We need to do some explicit focus-grabbing so that Firefox
+                # knows exactly where we are.  If we do this, we do not want
+                # to speak the object contents.
+                #
+                focusGrabbed = obj.component.grabFocus()
+                orca.setLocusOfFocus(None, obj, False)
+                speakContents = False
+
+                if obj.role == rolenames.ROLE_COMBO_BOX:
+                    obj = obj.child(0)
+
+                if obj.selection:
+                    for i in range(0, obj.childCount):
+                        if obj.selection.isChildSelected(i):
+                            obj = obj.child(i)
+                            break
+                    if i == obj.childCount - 1:
+                        obj = obj.child(0)
+
+            [obj, characterOffset] = self.findFirstCaretContext(obj, 0)
+            if obj and obj.role in [rolenames.ROLE_CHECK_BOX,
+                                    rolenames.ROLE_RADIO_BUTTON]:
+                # [[[TODO: HACK - JD.  Explicit focus-grabbing doesn't
+                # work on all objects.  But form fields by their very
+                # nature have text around them.  Therefore, let's try 
+                # quietly updating our caretContext to the text that's
+                # just after the form field we care about before setting
+                # the caret position to that form field.]]]
+                #
+                [nextObject, nextOffset] = \
+                                     self.findNextCaretInOrder(obj, -1, False)
+                if nextObject:
+                    self.setCaretPosition(nextObject, nextOffset)
+
+            elif obj and obj.role in [rolenames.ROLE_ENTRY,
+                                      rolenames.ROLE_PASSWORD_TEXT]:
+                # [[[TODO: HACK - JD.  If it's an entry, try to activate it
+                # through the AccessibleAction interface.  NOTE: This seems
+                # to work fine for single-line entries and fail for multi-
+                # line ones. ]]]
+                #
+                name = None
+                if obj.action:
+                    for i in range(0, obj.action.nActions):
+                        name = obj.action.getName(i)
+                        if name == "activate":
+                            break
+                    if name:
+                         success = obj.action.doAction(i)
+
+            self.setCaretPosition(obj, characterOffset)
+            self.updateBraille(obj)
+            if speakContents:
+                self.speakContents(self.getObjectContentsAtOffset(obj,
+                                                             characterOffset))
+        else:
+            # Translators: this is for navigating HTML content by
+            # moving from form field to form field.
+            #
+            speech.speak(_("No more form fields."))
+
     def goPreviousTable(self, inputEvent):
         obj = self.findPreviousRole([rolenames.ROLE_TABLE])
         if obj:
