[orca] Fix for bgo#586399 - Orca should provide support/access to "Mouse Overs" in web content.



commit 25c1c1d232cf84f430e9be7febf9232e0fc49acb
Author: Joanmarie Diggs <joanmarie diggs gmail com>
Date:   Fri Jul 17 20:18:53 2009 -0400

    Fix for bgo#586399 - Orca should provide support/access to "Mouse Overs" in web content.
    
    There is a new command which will move focus to and away from the object
    which appeared as a result of hovering the mouse over an item in web content.
    This command is bound to Orca+KP_Multiply for the Desktop layout and Orca+0
    in the laptop layout. The way you would take advantage of this command is to
    do the following:
    
    1. Move focus to an object which you know/suspect has an associated mouseover.
    
    2. Route the pointer to that object using Orca+KP_Divide/Orca+9. If a new
       mouseover object appears as a result, Orca should announce what it is.
    
    3. If you think the new object that appeared might be of interest, you can
       move focus to that new object with Orca+KP_Multiply/Orca+0.
    
    4. You should be able to interact with the new object like any other content.
       If you decide the mouse-over object is not of interest after all, or if
       the mouse-over object merely presented information and you're done reading
       it, you can press Orca+KP_Multiply/Orca+0 a second time. This will move
       the mouse pointer back to where it was before you routed it. The act of
       doing so should cause the mouse-over object to disappear. Focus will then
       be restored to the object you were on prior to accessing the mouse over.

 src/orca/default.py                                |   32 +++++-
 src/orca/scripts/toolkits/Gecko/script.py          |  129 ++++++++++++++++++++
 test/html/mouseover.html                           |   24 ++++
 .../firefox/mouseover_javascript_alert.py          |   94 ++++++++++++++
 test/keystrokes/firefox/mouseover_yahoo_menus.py   |  115 +++++++++++++++++
 5 files changed, 391 insertions(+), 3 deletions(-)
---
diff --git a/src/orca/default.py b/src/orca/default.py
index 0362241..e48f23c 100644
--- a/src/orca/default.py
+++ b/src/orca/default.py
@@ -127,6 +127,17 @@ class Script(script.Script):
         #
         self.attributeNamesDict = {}
 
+        # Keep track of the last time we issued a mouse routing command
+        # so that we can guess if a change resulted from our moving the
+        # pointer.
+        #
+        self.lastMouseRoutingTime = None
+
+        # The last location of the mouse, which we might want if routing
+        # the pointer elsewhere.
+        #
+        self.oldMouseCoordinates = [0, 0]
+
     def setupInputEventHandlers(self):
         """Defines InputEventHandler fields for this script that can be
         called by the key and braille bindings."""
@@ -1079,19 +1090,20 @@ class Script(script.Script):
 
         # We want the user to be able to combine modifiers with the
         # mouse click (e.g. to Shift+Click and select), therefore we
-        # do not "care" about the modifiers.
+        # do not "care" about the modifiers -- unless it's the Orca
+        # modifier.
         #
         keyBindings.add(
             keybindings.KeyBinding(
                 "KP_Divide",
-                settings.NO_MODIFIER_MASK,
+                settings.ORCA_MODIFIER_MASK,
                 settings.NO_MODIFIER_MASK,
                 self.inputEventHandlers["leftClickReviewItemHandler"]))
 
         keyBindings.add(
             keybindings.KeyBinding(
                 "KP_Multiply",
-                settings.NO_MODIFIER_MASK,
+                settings.ORCA_MODIFIER_MASK,
                 settings.NO_MODIFIER_MASK,
                 self.inputEventHandlers["rightClickReviewItemHandler"]))
 
@@ -5135,9 +5147,23 @@ class Script(script.Script):
 
         return True
 
+    def getAbsoluteMouseCoordinates(self):
+        """Gets the absolute position of the mouse pointer."""
+
+        import gtk
+        rootWindow = gtk.Window().get_screen().get_root_window()
+        x, y, modifiers = rootWindow.get_pointer()
+
+        return x, y
+
     def routePointerToItem(self, inputEvent=None):
         """Moves the mouse pointer to the current item."""
 
+        # Store the original location for scripts which want to restore
+        # it later.
+        #
+        self.oldMouseCoordinates = self.getAbsoluteMouseCoordinates()
+        self.lastMouseRoutingTime = time.time()
         if self.flatReviewContext:
             self.flatReviewContext.routeToCurrent()
         else:
diff --git a/src/orca/scripts/toolkits/Gecko/script.py b/src/orca/scripts/toolkits/Gecko/script.py
index 9310fa6..b9c5141 100644
--- a/src/orca/scripts/toolkits/Gecko/script.py
+++ b/src/orca/scripts/toolkits/Gecko/script.py
@@ -50,6 +50,7 @@ import urlparse
 import orca.braille as braille
 import orca.debug as debug
 import orca.default as default
+import orca.eventsynthesizer as eventsynthesizer
 import orca.input_event as input_event
 import orca.keybindings as keybindings
 import orca.liveregions as liveregions
@@ -263,6 +264,15 @@ class Script(default.Script):
         self.savedEnabledSpokenTextAttributes = None
         self.savedAllTextAttributes = None
 
+        # Keep track of the last object which appeared as a result of
+        # the user routing the mouse pointer over an object. Also keep
+        # track of the object which is associated with the mouse over
+        # so that we can restore focus to it if need be.
+        #
+        self.lastMouseOverObject = None
+        self.preMouseOverContext = [None, -1]
+        self.inMouseOverObject = False
+
     def activate(self):
         """Called when this script is activated."""
         self.savedEnabledBrailledTextAttributes = \
@@ -520,6 +530,19 @@ class Script(default.Script):
                 #
                 _("Speaks entire document."))
 
+        self.inputEventHandlers["moveToMouseOverHandler"] = \
+            input_event.InputEventHandler(
+                Script.moveToMouseOver,
+                # Translators: hovering the mouse over certain objects
+                # on a web page causes a new object to appear such as
+                # a pop-up menu. This command will move the user to the
+                # object which just appeared as a result of the user
+                # hovering the mouse. If the user is already in the
+                # mouse over object, this command will hide the mouse
+                # over and return the user to the object he/she was in.
+                #
+                _("Moves focus into and away from the current mouse over."))
+
     def getListeners(self):
         """Sets up the AT-SPI event listeners for this script.
         """
@@ -708,6 +731,22 @@ class Script(default.Script):
                 settings.ORCA_MODIFIER_MASK,
                 self.inputEventHandlers["goPreviousObjectInOrderHandler"]))
 
+        if orca.settings.keyboardLayout == \
+                orca.settings.GENERAL_KEYBOARD_LAYOUT_DESKTOP:
+            keyBindings.add(
+                keybindings.KeyBinding(
+                    "KP_Multiply",
+                    settings.defaultModifierMask,
+                    settings.ORCA_MODIFIER_MASK,
+                    self.inputEventHandlers["moveToMouseOverHandler"]))
+        else:
+            keyBindings.add(
+                keybindings.KeyBinding(
+                    "0",
+                    settings.defaultModifierMask,
+                    settings.ORCA_MODIFIER_MASK,
+                    self.inputEventHandlers["moveToMouseOverHandler"]))
+
         if script_settings.controlCaretNavigation:
             for keyBinding in self.__getArrowBindings().keyBindings:
                 keyBindings.add(keyBinding)
@@ -1490,6 +1529,14 @@ class Script(default.Script):
         if not event.source.getState().contains(pyatspi.STATE_EDITABLE):
             self._guessedLabels = {}
 
+            if self.inMouseOverObject:
+                obj = self.lastMouseOverObject
+                while obj and (obj != obj.parent):
+                    if self.isSameObject(event.source, obj):
+                        self.restorePreMouseOverContext()
+                        break
+                    obj = obj.parent
+
         default.Script.onTextDeleted(self, event)
 
     def onTextInserted(self, event):
@@ -1553,6 +1600,27 @@ class Script(default.Script):
         if event.any_data is None:
             return
 
+        # If we just routed the mouse pointer to our current location,
+        # we should say something about what resulted.
+        #
+        if self.lastMouseRoutingTime \
+           and 0 < time.time() - self.lastMouseRoutingTime < 1 \
+           and event.type.startswith("object:children-changed:add"):
+            utterances = []
+            # Translators: Orca has a command that moves the mouse
+            # pointer to the current location on a web page. If
+            # moving the mouse pointer caused an item to appear
+            # such as a pop-up menu, we want to present that fact.
+            #
+            utterances.append(_("New item has been added"))
+            utterances.extend(
+                self.speechGenerator.generateSpeech(event.any_data,
+                                                    force = True))
+            speech.speak(utterances)
+            self.lastMouseOverObject = event.any_data
+            self.preMouseOverContext = self.getCaretContext()
+            return
+
         # handle live region events
         if self.handleAsLiveRegion(event):
             self.liveMngr.handleEvent(event)
@@ -5788,6 +5856,67 @@ class Script(default.Script):
                                                       characterOffset,
                                                       characterOffset + 1))
 
+    def moveToMouseOver(self, inputEvent):
+        """Positions the caret offset to the next character or object
+        in the mouse over which has just appeared.
+        """
+
+        if not self.lastMouseOverObject:
+            # Translators: hovering the mouse over certain objects on a
+            # web page causes a new object to appear such as a pop-up
+            # menu. Orca has a command will move the user to the object
+            # which just appeared as a result of the user hovering the
+            # mouse. If this command fails, Orca will present this message.
+            #
+            message = _("Mouse over object not found.")
+            speech.speak(message)
+            return
+
+        if not self.inMouseOverObject:
+            obj = self.lastMouseOverObject
+            offset = 0
+            if obj and not obj.getState().contains(pyatspi.STATE_FOCUSABLE):
+                [obj, offset] = self.findFirstCaretContext(obj, offset)
+
+            if obj and obj.getState().contains(pyatspi.STATE_FOCUSABLE):
+                obj.queryComponent().grabFocus()
+            elif obj:
+                contents = self.getObjectContentsAtOffset(obj, offset)
+                # If we don't have anything to say, let's try one more
+                # time.
+                #
+                if len(contents) == 1 and not contents[0][3].strip():
+                    [obj, offset] = self.findNextCaretInOrder(obj, offset)
+                    contents = self.getObjectContentsAtOffset(obj, offset)
+                self.setCaretPosition(obj, offset)
+                self.speakContents(contents)
+                self.updateBraille(obj)
+            self.inMouseOverObject = True
+        else:
+            # Route the mouse pointer where it was before both to "clean up
+            # after ourselves" and also to get the mouse over object to go
+            # away.
+            #
+            x, y = self.oldMouseCoordinates
+            eventsynthesizer.routeToPoint(x, y)
+            self.restorePreMouseOverContext()
+
+    def restorePreMouseOverContext(self):
+        """Cleans things up after a mouse-over object has been hidden."""
+
+        obj, offset = self.preMouseOverContext
+        if obj and not obj.getState().contains(pyatspi.STATE_FOCUSABLE):
+            [obj, offset] = self.findFirstCaretContext(obj, offset)
+
+        if obj and obj.getState().contains(pyatspi.STATE_FOCUSABLE):
+            obj.queryComponent().grabFocus()
+        elif obj:
+            self.setCaretPosition(obj, offset)
+            self.speakContents(self.getObjectContentsAtOffset(obj, offset))
+            self.updateBraille(obj)
+        self.inMouseOverObject = False
+        self.lastMouseOverObject = None
+
     def goNextCharacter(self, inputEvent):
         """Positions the caret offset to the next character or object
         in the document window.
diff --git a/test/html/mouseover.html b/test/html/mouseover.html
new file mode 100644
index 0000000..774275f
--- /dev/null
+++ b/test/html/mouseover.html
@@ -0,0 +1,24 @@
+<html>
+<head><title>MouseOvers</title></head>
+<script language="JavaScript">
+<!--
+var myImg = document.getElementById('myImage');
+ 
+function myMessage() {
+    alert('Welcome to mouseover-enabled Orca!');
+}
+ 
+if(myImg.addEventListener) {
+    myImg.addEventListener('mouseover', myMessage, false);
+}
+else { 
+    myImg.onmouseover = myMessage;
+}
+//-->
+</script>
+<body>
+<p>Mouseover which will present an traditional alert:</p>
+<p><img id="myImage" src="orca-for-tests.jpg" alt="Orca Logo" onMouseOver="myMessage();"></p>
+</body>
+</html>
+
diff --git a/test/keystrokes/firefox/mouseover_javascript_alert.py b/test/keystrokes/firefox/mouseover_javascript_alert.py
new file mode 100644
index 0000000..ccf6c83
--- /dev/null
+++ b/test/keystrokes/firefox/mouseover_javascript_alert.py
@@ -0,0 +1,94 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/python
+
+"""Test of Orca's support for mouseovers.
+"""
+
+from macaroon.playback import *
+import utils
+
+sequence = MacroSequence()
+
+########################################################################
+# We wait for the focus to be on the Firefox window as well as for focus
+# to move to the "inline: Tab Panel Example 1" frame.
+#
+sequence.append(WaitForWindowActivate(utils.firefoxFrameNames, None))
+
+########################################################################
+# Load the UIUC Tab Panel demo.
+#
+sequence.append(KeyComboAction("<Control>l"))
+sequence.append(WaitForFocus(acc_role=pyatspi.ROLE_ENTRY))
+sequence.append(TypeAction(utils.htmlURLPrefix + "mouseover.html"))
+sequence.append(KeyComboAction("Return"))
+sequence.append(WaitForDocLoad())
+
+########################################################################
+# Press Control+Home to move to the top.
+#
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("<Control>Home"))
+sequence.append(utils.AssertPresentationAction(
+    "Top of file", 
+    ["BRAILLE LINE:  'Mouseover which will present an traditional alert:'",
+     "     VISIBLE:  'Mouseover which will present an ', cursor=1",
+     "SPEECH OUTPUT: 'Mouseover which will present an traditional alert:'"]))
+
+########################################################################
+# Down Arrow to the image.
+#
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("Down"))
+sequence.append(utils.AssertPresentationAction(
+    "Down Arrow to Image",
+    ["BRAILLE LINE:  'Orca Logo Image'",
+     "     VISIBLE:  'Orca Logo Image', cursor=1",
+     "SPEECH OUTPUT: 'Orca Logo image'"]))
+
+########################################################################
+# Route the mouse pointer to the image, triggering a javascript alert.
+#
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyPressAction(0, None, "KP_Insert"))
+sequence.append(KeyComboAction("KP_Divide"))
+sequence.append(KeyReleaseAction(0, None, "KP_Insert"))
+sequence.append(utils.AssertPresentationAction(
+    "Route the pointer to the image",
+    ["BRAILLE LINE:  '" + utils.firefoxAppNames + " Application \[JavaScript Application\] Dialog'",
+     "     VISIBLE:  '[JavaScript Application] Dialog', cursor=1",
+     "BRAILLE LINE:  '" + utils.firefoxAppNames + " Application \[JavaScript Application\] Dialog OK Button'",
+     "     VISIBLE:  'OK Button', cursor=1",
+     "SPEECH OUTPUT: 'New item has been added'",
+     "SPEECH OUTPUT: '[JavaScript Application] Welcome to mouseover-enabled Orca!'",
+     "SPEECH OUTPUT: 'OK button'"]))
+
+########################################################################
+# Dismiss the alert.
+#
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("Escape"))
+sequence.append(utils.AssertPresentationAction(
+    "Escape to dismiss the dialog.",
+    ["BRAILLE LINE:  '" + utils.firefoxAppNames + " Application MouseOvers - " + utils.firefoxFrameNames + " Frame'",
+     "     VISIBLE:  'MouseOvers - " + utils.firefoxFrameNames + " Fra', cursor=1",
+     "BRAILLE LINE:  'Orca Logo Image'",
+     "     VISIBLE:  'Orca Logo Image', cursor=1",
+     "SPEECH OUTPUT: 'MouseOvers - " + utils.firefoxFrameNames + " frame'",
+     "SPEECH OUTPUT: 'Orca Logo image'"]))
+
+########################################################################
+# Close the demo
+#
+sequence.append(KeyComboAction("<Control>l"))
+sequence.append(WaitForFocus(acc_role=pyatspi.ROLE_ENTRY))
+sequence.append(TypeAction("about:blank"))
+sequence.append(KeyComboAction("Return"))
+sequence.append(WaitForDocLoad())
+# Just a little extra wait to let some events get through.
+#
+sequence.append(PauseAction(3000))
+
+sequence.append(utils.AssertionSummaryAction())
+
+sequence.start()
diff --git a/test/keystrokes/firefox/mouseover_yahoo_menus.py b/test/keystrokes/firefox/mouseover_yahoo_menus.py
new file mode 100644
index 0000000..ef6a1bc
--- /dev/null
+++ b/test/keystrokes/firefox/mouseover_yahoo_menus.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+#!/usr/bin/python
+
+"""Test of Yahoo's menus accessed via mouseover
+"""
+
+from macaroon.playback import *
+import utils
+
+sequence = MacroSequence()
+
+########################################################################
+# We wait for the focus to be on the Firefox window as well as for focus
+# to move to the "inline: Tab Panel Example 1" frame.
+#
+sequence.append(WaitForWindowActivate(utils.firefoxFrameNames, None))
+
+########################################################################
+# Load the UIUC Tab Panel demo.
+#
+sequence.append(KeyComboAction("<Control>l"))
+sequence.append(WaitForFocus(acc_role=pyatspi.ROLE_ENTRY))
+sequence.append(TypeAction("http://developer.yahoo.com/yui/examples/menu/leftnavfrommarkupwithanim_source.html";))
+sequence.append(KeyComboAction("Return"))
+sequence.append(WaitForDocLoad())
+
+sequence.append(PauseAction(5000))
+
+########################################################################
+# Navigate to the Communication menu (which is actually a link).
+#
+sequence.append(KeyComboAction("Tab"))
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("Tab"))
+sequence.append(utils.AssertPresentationAction(
+    "Navigate to Communications",
+    ["BRAILLE LINE:  'Communication'",
+     "     VISIBLE:  'Communication', cursor=1",
+     "SPEECH OUTPUT: 'Communication link'"]))
+
+########################################################################
+# Route the mouse pointer to Communication.
+#
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyPressAction(0, None, "KP_Insert"))
+sequence.append(KeyComboAction("KP_Divide"))
+sequence.append(KeyReleaseAction(0, None, "KP_Insert"))
+sequence.append(utils.AssertPresentationAction(
+    "Route the pointer to Communications",
+    ["BUG? - It would be nice to filter out multiple events, but until we figure out the magical way to identify what not to report, we report the resulting new children. All of them.",
+     "SPEECH OUTPUT: 'New item has been added PIM link'",
+     "SPEECH OUTPUT: 'New item has been added section'",
+     "SPEECH OUTPUT: 'New item has been added section'"]))
+
+########################################################################
+# Move focus into the new child.
+#
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyPressAction(0, None, "KP_Insert"))
+sequence.append(KeyComboAction("KP_Multiply"))
+sequence.append(KeyReleaseAction(0, None, "KP_Insert"))
+sequence.append(utils.AssertPresentationAction(
+    "Move focus inside Communications menu",
+    ["BRAILLE LINE:  'Communication 360°'",
+     "     VISIBLE:  'Communication 360°', cursor=15",
+     "SPEECH OUTPUT: '360° link'"]))
+
+########################################################################
+# Navigate amongst the menu items.
+#
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("Tab"))
+sequence.append(utils.AssertPresentationAction(
+    "1. Tab to menu item",
+    ["BRAILLE LINE:  'Alerts'",
+     "     VISIBLE:  'Alerts', cursor=1",
+     "SPEECH OUTPUT: 'Alerts link'"]))
+
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyComboAction("Tab"))
+sequence.append(utils.AssertPresentationAction(
+    "1. Tab to menu item",
+    ["BRAILLE LINE:  'Avatars'",
+     "     VISIBLE:  'Avatars', cursor=1",
+     "SPEECH OUTPUT: 'Avatars link'"]))
+
+########################################################################
+# Restore the mouse pointer to the previous location to close the
+# Communications menu and land back on the Communications link.
+#
+sequence.append(utils.StartRecordingAction())
+sequence.append(KeyPressAction(0, None, "KP_Insert"))
+sequence.append(KeyComboAction("KP_Multiply"))
+sequence.append(KeyReleaseAction(0, None, "KP_Insert"))
+sequence.append(utils.AssertPresentationAction(
+    "Close the Communications menu",
+    ["BRAILLE LINE:  'Communication 360°'",
+     "     VISIBLE:  'Communication 360°', cursor=1",
+     "SPEECH OUTPUT: 'Communication link'"]))
+
+########################################################################
+# Close the demo
+#
+sequence.append(KeyComboAction("<Control>l"))
+sequence.append(WaitForFocus(acc_role=pyatspi.ROLE_ENTRY))
+sequence.append(TypeAction("about:blank"))
+sequence.append(KeyComboAction("Return"))
+sequence.append(WaitForDocLoad())
+# Just a little extra wait to let some events get through.
+#
+sequence.append(PauseAction(3000))
+
+sequence.append(utils.AssertionSummaryAction())
+
+sequence.start()



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