[orca] Add initial Chromium script



commit 0391f91ad855426edc216f8ff7c41263a1738511
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Thu Nov 1 14:04:20 2018 +0100

    Add initial Chromium script
    
    Please note: ATK support in Chromium needs much work. Until that work has
    been done, Orca will not be able to provide access to Chromium. This
    script is very much a work in progress and not yet ready for end-user
    testing.

 configure.ac                                       |   1 +
 src/orca/scripts/toolkits/Chromium/Makefile.am     |   8 +
 src/orca/scripts/toolkits/Chromium/__init__.py     |  22 ++
 .../scripts/toolkits/Chromium/braille_generator.py |  60 ++++
 src/orca/scripts/toolkits/Chromium/script.py       | 368 +++++++++++++++++++++
 .../scripts/toolkits/Chromium/script_utilities.py  | 159 +++++++++
 .../scripts/toolkits/Chromium/speech_generator.py  |  60 ++++
 src/orca/scripts/toolkits/Makefile.am              |   2 +-
 src/orca/scripts/toolkits/__init__.py              |   1 +
 9 files changed, 680 insertions(+), 1 deletion(-)
---
diff --git a/configure.ac b/configure.ac
index 8d701dd69..2454476e4 100644
--- a/configure.ac
+++ b/configure.ac
@@ -122,6 +122,7 @@ src/orca/scripts/apps/xfwm4/Makefile
 src/orca/scripts/terminal/Makefile
 src/orca/scripts/web/Makefile
 src/orca/scripts/toolkits/Makefile
+src/orca/scripts/toolkits/Chromium/Makefile
 src/orca/scripts/toolkits/Gecko/Makefile
 src/orca/scripts/toolkits/J2SE-access-bridge/Makefile
 src/orca/scripts/toolkits/clutter/Makefile
diff --git a/src/orca/scripts/toolkits/Chromium/Makefile.am b/src/orca/scripts/toolkits/Chromium/Makefile.am
new file mode 100644
index 000000000..64e94b895
--- /dev/null
+++ b/src/orca/scripts/toolkits/Chromium/Makefile.am
@@ -0,0 +1,8 @@
+orca_python_PYTHON = \
+       __init__.py \
+       braille_generator.py \
+       script.py \
+       script_utilities.py \
+       speech_generator.py
+
+orca_pythondir=$(pkgpythondir)/scripts/toolkits/Chromium
diff --git a/src/orca/scripts/toolkits/Chromium/__init__.py b/src/orca/scripts/toolkits/Chromium/__init__.py
new file mode 100644
index 000000000..af00bc874
--- /dev/null
+++ b/src/orca/scripts/toolkits/Chromium/__init__.py
@@ -0,0 +1,22 @@
+# Orca
+#
+# Copyright 2018 Igalia, S.L.
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA  02110-1301 USA.
+
+"""Custom script for Chromium."""
+
+from .script import Script
diff --git a/src/orca/scripts/toolkits/Chromium/braille_generator.py 
b/src/orca/scripts/toolkits/Chromium/braille_generator.py
new file mode 100644
index 000000000..891a30a45
--- /dev/null
+++ b/src/orca/scripts/toolkits/Chromium/braille_generator.py
@@ -0,0 +1,60 @@
+# Orca
+#
+# Copyright 2018 Igalia, S.L.
+#
+# Author: Joanmarie Diggs <jdiggs igalia com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA  02110-1301 USA.
+
+"""Custom braille generator for Chromium."""
+
+# Please note: ATK support in Chromium needs much work. Until that work has been
+# done, Orca will not be able to provide access to Chromium. This generator is a
+# work in progress.
+
+__id__        = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (c) 2018 Igalia, S.L."
+__license__   = "LGPL"
+
+import pyatspi
+
+from orca import debug
+from orca import orca_state
+from orca.scripts import web
+
+
+class BrailleGenerator(web.BrailleGenerator):
+
+    def __init__(self, script):
+        super().__init__(script)
+
+    def generateBraille(self, obj, **args):
+        if self._script.utilities.inDocumentContent(obj):
+            return super().generateBraille(obj, **args)
+
+        oldRole = None
+        if self._script.utilities.treatAsMenu(obj):
+            msg = "CHROMIUM: HACK? Displaying menu item as menu %s" % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            oldRole = self._overrideRole(pyatspi.ROLE_MENU, args)
+
+        result = super().generateBraille(obj, **args)
+        if oldRole is not None:
+            self._restoreRole(oldRole, args)
+
+        return result
diff --git a/src/orca/scripts/toolkits/Chromium/script.py b/src/orca/scripts/toolkits/Chromium/script.py
new file mode 100644
index 000000000..f87a0e28a
--- /dev/null
+++ b/src/orca/scripts/toolkits/Chromium/script.py
@@ -0,0 +1,368 @@
+# Orca
+#
+# Copyright 2018 Igalia, S.L.
+#
+# Author: Joanmarie Diggs <jdiggs igalia com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA  02110-1301 USA.
+
+"""Custom script for Chromium."""
+
+# Please note: ATK support in Chromium needs much work. Until that work has been
+# done, Orca will not be able to provide access to Chromium. This script is a
+# work in progress.
+
+__id__        = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (c) 2018 Igalia, S.L."
+__license__   = "LGPL"
+
+import pyatspi
+
+from orca import debug
+from orca import orca
+from orca.scripts import default
+from orca.scripts import web
+from .braille_generator import BrailleGenerator
+from .script_utilities import Utilities
+from .speech_generator import SpeechGenerator
+
+
+class Script(web.Script):
+
+    def __init__(self, app):
+        super().__init__(app)
+
+        self.presentIfInactive = False
+
+        # Normally we don't cache the name, because we cannot count on apps and
+        # tookits emitting name-changed events (needed by AT-SPI2 to update the
+        # name so that we don't have stale values). However, if we don't cache
+        # the name, we wind up with dead accessibles in (at least) the search bar
+        # popup. For now, cache the name to get things working and clear the cache
+        # for objects we plan to present. http://crbug.com/896706
+        app.setCacheMask(pyatspi.cache.DEFAULT ^ pyatspi.cache.CHILDREN)
+
+    def getBrailleGenerator(self):
+        """Returns the braille generator for this script."""
+
+        return BrailleGenerator(self)
+
+    def getSpeechGenerator(self):
+        """Returns the speech generator for this script."""
+
+        return SpeechGenerator(self)
+
+    def getUtilities(self):
+        """Returns the utilites for this script."""
+
+        return Utilities(self)
+
+    def isActivatableEvent(self, event):
+        """Returns True if this event should activate this script."""
+
+        if event.type == "window:activate":
+            return self.utilities.canBeActiveWindow(event.source)
+
+        return super().isActivatableEvent(event)
+
+    def locusOfFocusChanged(self, event, oldFocus, newFocus):
+        """Handles changes of focus of interest to the script."""
+
+        # Normally we don't cache the name, because we cannot count on apps and
+        # tookits emitting name-changed events (needed by AT-SPI2 to update the
+        # name so that we don't have stale values). However, if we don't cache
+        # the name, we wind up with dead accessibles in (at least) the search bar
+        # popup. For now, cache the name to get things working and clear the cache
+        # for objects we plan to present. Mind you, clearing the cache on objects
+        # with many descendants can cause AT-SPI2 to become non-responsive so try
+        # to guess what NOT to clear the cache for. http://crbug.com/896706
+        doNotClearCacheFor = [pyatspi.ROLE_DIALOG,
+                              pyatspi.ROLE_FRAME,
+                              pyatspi.ROLE_LIST_BOX,
+                              pyatspi.ROLE_MENU,
+                              pyatspi.ROLE_REDUNDANT_OBJECT,
+                              pyatspi.ROLE_TABLE,
+                              pyatspi.ROLE_TREE,
+                              pyatspi.ROLE_TREE_TABLE,
+                              pyatspi.ROLE_UNKNOWN,
+                              pyatspi.ROLE_WINDOW]
+        if newFocus.getRole() not in doNotClearCacheFor:
+            msg = "CHROMIUM: CANNOT CACHE NAME HACK: Clearing cache for %s" % newFocus
+            debug.println(debug.LEVEL_INFO, msg, True)
+            newFocus.clearCache()
+
+        # HACK: Remove this once Chromium has support for input events.
+        if oldFocus and newFocus \
+           and oldFocus.getRole() != pyatspi.ROLE_FRAME \
+           and oldFocus.getApplication() == newFocus.getApplication() == self.app \
+           and not self.utilities.lastInputEventCameFromThisApp():
+            msg = "CHROMIUM: NO INPUT EVENT PRESENTATION INTERRUPT HACK"
+            debug.println(debug.LEVEL_INFO, msg, True)
+            self.presentationInterrupt()
+
+        if super().locusOfFocusChanged(event, oldFocus, newFocus):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.locusOfFocusChanged(self, event, oldFocus, newFocus)
+
+    def onActiveChanged(self, event):
+        """Callback for object:state-changed:active accessibility events."""
+
+        if super().onActiveChanged(event):
+            return
+
+        role = event.source.getRole()
+        if event.detail1 and role == pyatspi.ROLE_FRAME \
+           and not self.utilities.canBeActiveWindow(event.source):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onActiveChanged(self, event)
+
+        # HACK: Remove this once Chromium emits focus changes after window activation.
+        if event.detail1 and role == pyatspi.ROLE_FRAME:
+            focusedObject = self.utilities.focusedObject(event.source)
+            msg = "CHROMIUM: NO INITIAL FOCUS HACK. Focused object: %s" % focusedObject
+            debug.println(debug.LEVEL_INFO, msg, True)
+            if focusedObject:
+                orca.setLocusOfFocus(event, focusedObject)
+
+    def onActiveDescendantChanged(self, event):
+        """Callback for object:active-descendant-changed accessibility events."""
+
+        if super().onActiveDescendantChanged(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onActiveDescendantChanged(self, event)
+
+    def onBusyChanged(self, event):
+        """Callback for object:state-changed:busy accessibility events."""
+
+        if super().onBusyChanged(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onBusyChanged(self, event)
+
+    def onCaretMoved(self, event):
+        """Callback for object:text-caret-moved accessibility events."""
+
+        if super().onCaretMoved(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onCaretMoved(self, event)
+
+    def onCheckedChanged(self, event):
+        """Callback for object:state-changed:checked accessibility events."""
+
+        if super().onCheckedChanged(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onCheckedChanged(self, event)
+
+    def onChildrenChanged(self, event):
+        """Callback for object:children-changed accessibility events."""
+
+        if super().onChildrenChanged(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onChildrenChanged(self, event)
+
+    def onDocumentLoadComplete(self, event):
+        """Callback for document:load-complete accessibility events."""
+
+        if super().onDocumentLoadComplete(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onDocumentLoadComplete(self, event)
+
+    def onDocumentLoadStopped(self, event):
+        """Callback for document:load-stopped accessibility events."""
+
+        if super().onDocumentLoadStopped(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onDocumentLoadStopped(self, event)
+
+    def onDocumentReload(self, event):
+        """Callback for document:reload accessibility events."""
+
+        if super().onDocumentReload(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onDocumentReload(self, event)
+
+    def onFocus(self, event):
+        """Callback for focus: accessibility events."""
+
+        # This event is deprecated. We should get object:state-changed:focused
+        # events instead.
+
+        if super().onFocus(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onFocus(self, event)
+
+    def onFocusedChanged(self, event):
+        """Callback for object:state-changed:focused accessibility events."""
+
+        if super().onFocusedChanged(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onFocusedChanged(self, event)
+
+    def onMouseButton(self, event):
+        """Callback for mouse:button accessibility events."""
+
+        if super().onMouseButton(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onMouseButton(self, event)
+
+    def onNameChanged(self, event):
+        """Callback for object:property-change:accessible-name events."""
+
+        if super().onNameChanged(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onNameChanged(self, event)
+
+    def onSelectedChanged(self, event):
+        """Callback for object:state-changed:selected accessibility events."""
+
+        if super().onSelectedChanged(event):
+            return
+
+        # Other apps and toolkits implement the selection interface, which is
+        # what we use to present active-descendanty selection changes, leaving
+        # state-changed:selected for notifications related to toggling the
+        # selected state of the currently-focused item (e.g. pressing ctrl+space
+        # in a file explorer). While handling active-descendanty changes here is
+        # not technically a HACK, once Chromium implements the selection interface,
+        # we should remove this code and defer to Orca's default handling.
+        if event.detail1 and not self.utilities.isLayoutOnly(event.source) \
+           and not "Selection" in pyatspi.listInterfaces(event.source.parent) \
+           and self.utilities.canBeActiveWindow(self.utilities.topLevelObject(event.source)):
+            msg = "CHROMIUM: NO SELECTION IFACE HACK: Setting %s to locusOfFocus" % event.source
+            debug.println(debug.LEVEL_INFO, msg, True)
+            orca.setLocusOfFocus(event, event.source)
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onSelectedChanged(self, event)
+
+    def onSelectionChanged(self, event):
+        """Callback for object:selection-changed accessibility events."""
+
+        if super().onSelectionChanged(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onSelectionChanged(self, event)
+
+    def onShowingChanged(self, event):
+        """Callback for object:state-changed:showing accessibility events."""
+
+        if super().onShowingChanged(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onShowingChanged(self, event)
+
+    def onTextDeleted(self, event):
+        """Callback for object:text-changed:delete accessibility events."""
+
+        if super().onTextDeleted(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onTextDeleted(self, event)
+
+    def onTextInserted(self, event):
+        """Callback for object:text-changed:insert accessibility events."""
+
+        if super().onTextInserted(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onTextInserted(self, event)
+
+    def onTextSelectionChanged(self, event):
+        """Callback for object:text-selection-changed accessibility events."""
+
+        if super().onTextSelectionChanged(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onTextSelectionChanged(self, event)
+
+    def onWindowActivated(self, event):
+        """Callback for window:activate accessibility events."""
+
+        if not self.utilities.canBeActiveWindow(event.source):
+            return
+
+        if super().onWindowActivated(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onWindowActivated(self, event)
+
+    def onWindowDeactivated(self, event):
+        """Callback for window:deactivate accessibility events."""
+
+        if super().onWindowDeactivated(event):
+            return
+
+        msg = "CHROMIUM: Passing along event to default script"
+        debug.println(debug.LEVEL_INFO, msg, True)
+        default.Script.onWindowDeactivated(self, event)
+
diff --git a/src/orca/scripts/toolkits/Chromium/script_utilities.py 
b/src/orca/scripts/toolkits/Chromium/script_utilities.py
new file mode 100644
index 000000000..54b0ef849
--- /dev/null
+++ b/src/orca/scripts/toolkits/Chromium/script_utilities.py
@@ -0,0 +1,159 @@
+# Orca
+#
+# Copyright 2018 Igalia, S.L.
+#
+# Author: Joanmarie Diggs <jdiggs igalia com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA  02110-1301 USA.
+
+"""Custom script utilities for Chromium"""
+
+# Please note: ATK support in Chromium needs much work. Until that work has been
+# done, Orca will not be able to provide access to Chromium. These utilities are
+# a work in progress.
+
+__id__        = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (c) 2018 Igalia, S.L."
+__license__   = "LGPL"
+
+import pyatspi
+
+from orca import debug
+from orca import orca_state
+from orca.scripts import web
+
+
+class Utilities(web.Utilities):
+
+    def __init__(self, script):
+        super().__init__(script)
+        self._documentsEmbeddedBy = {} # Needed for HACK
+
+    def clearCachedObjects(self):
+        super().clearCachedObjects()
+        self._documentsEmbeddedBy = {} # Needed for HACK
+
+    # So far, this is only needed by _getDocumentsEmbeddedBy. Hopefully we
+    # can remove this method once we remove the other one.
+    def _isTopLevelDocument(self, obj):
+        if not obj or self.isDead(obj):
+            return False
+
+        if not self.isDocument(obj):
+            return False
+
+        if obj.getRole() == pyatspi.ROLE_DOCUMENT_FRAME:
+            return False
+
+        if obj.parent and obj.parent.getRole() == pyatspi.ROLE_INTERNAL_FRAME:
+            return False
+
+        if not self.isShowingAndVisible(obj):
+            return False
+
+        return True
+
+    def _getDocumentsEmbeddedBy(self, frame):
+        result = super()._getDocumentsEmbeddedBy(frame)
+        if result:
+            return result
+
+        # HACK: This tree dive is not efficient and should be removed once Chromium
+        # implements support for the embeds/embedded-by relation pair.
+        cached = self._documentsEmbeddedBy.get(hash(frame), [])
+        result = list(filter(self._isTopLevelDocument, cached))
+        if not result:
+            documents = pyatspi.findAllDescendants(frame, self.isDocument)
+            result = list(filter(self._isTopLevelDocument, documents))
+            msg = "CHROMIUM: NO EMBEDDED RELATION HACK: %s has %i docs." % (frame, len(result))
+            debug.println(debug.LEVEL_INFO, msg, True)
+
+        self._documentsEmbeddedBy[hash(frame)] = result
+        return result
+
+    def isZombie(self, obj):
+        if not super().isZombie(obj):
+            return False
+
+        # Things (so far) seem to work as expected for document content.
+        if self.inDocumentContent(obj):
+            return True
+
+        # HACK for other items, including (though possibly not limited to) menu items
+        # (e.g. when you press Alt+F and arrow) and the location bar popup.
+        try:
+            index = obj.getIndexInParent()
+        except:
+            msg = "CHROMIUM: Exception getting index in parent for %s" % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return True
+
+        if index == -1 and self.isShowingAndVisible(obj):
+            msg = "CHROMIUM: INDEX IN PARENT OF -1 HACK: Ignoring bad index of %s" % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return False
+
+        return True
+
+    def _isFrameContainerForBrowserUIPopUp(self, frame):
+        if not frame or self.isDead(frame):
+            return False
+
+        # So far, the frame containers which lack the active state also lack names.
+        # Tree diving can be expensive....
+        if frame.name:
+            return False
+
+        roles = [pyatspi.ROLE_LIST_BOX, pyatspi.ROLE_MENU]
+        child = pyatspi.findDescendant(frame, lambda x: x and x.getRole() in roles)
+        return child and not self.inDocumentContent(child)
+
+    def canBeActiveWindow(self, window, clearCache=True):
+        if super().canBeActiveWindow(window, clearCache):
+            return True
+
+        if window and window.toolkitName != "Chromium":
+            return False
+
+        # HACK: Remove this once Chromium adds active state to popup frames.
+        if self._isFrameContainerForBrowserUIPopUp(window):
+            msg = "CHROMIUM: POPUP MISSING STATE ACTIVE HACK: %s can be active window" % window
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return True
+
+        return False
+
+    def treatAsMenu(self, obj):
+        if not obj:
+            return False
+
+        try:
+            role = obj.getRole()
+            state = obj.getState()
+        except:
+            msg = "ERROR: Exception getting role and state for %s" % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            return False
+
+        # Unlike other apps and toolkits, submenus in Chromium have the menu item
+        # role rather than the menu role, but we can identify them as submenus via
+        # the has-popup state.
+        if role == pyatspi.ROLE_MENU_ITEM:
+            return state.contains(pyatspi.STATE_HAS_POPUP)
+
+        return False
diff --git a/src/orca/scripts/toolkits/Chromium/speech_generator.py 
b/src/orca/scripts/toolkits/Chromium/speech_generator.py
new file mode 100644
index 000000000..c7b2f9daf
--- /dev/null
+++ b/src/orca/scripts/toolkits/Chromium/speech_generator.py
@@ -0,0 +1,60 @@
+# Orca
+#
+# Copyright 2018 Igalia, S.L.
+#
+# Author: Joanmarie Diggs <jdiggs igalia com>
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public
+# License along with this library; if not, write to the
+# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
+# Boston MA  02110-1301 USA.
+
+"""Custom speech generator for Chromium."""
+
+# Please note: ATK support in Chromium needs much work. Until that work has been
+# done, Orca will not be able to provide access to Chromium. This generator is a
+# work in progress.
+
+__id__        = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (c) 2018 Igalia, S.L."
+__license__   = "LGPL"
+
+import pyatspi
+
+from orca import debug
+from orca import orca_state
+from orca.scripts import web
+
+
+class SpeechGenerator(web.SpeechGenerator):
+
+    def __init__(self, script):
+        super().__init__(script)
+
+    def generateSpeech(self, obj, **args):
+        if self._script.utilities.inDocumentContent(obj):
+            return super().generateSpeech(obj, **args)
+
+        oldRole = None
+        if self._script.utilities.treatAsMenu(obj):
+            msg = "CHROMIUM: HACK? Speaking menu item as menu %s" % obj
+            debug.println(debug.LEVEL_INFO, msg, True)
+            oldRole = self._overrideRole(pyatspi.ROLE_MENU, args)
+
+        result = super().generateSpeech(obj, **args)
+        if oldRole is not None:
+            self._restoreRole(oldRole, args)
+
+        return result
diff --git a/src/orca/scripts/toolkits/Makefile.am b/src/orca/scripts/toolkits/Makefile.am
index 9c129108d..54de4c84b 100644
--- a/src/orca/scripts/toolkits/Makefile.am
+++ b/src/orca/scripts/toolkits/Makefile.am
@@ -1,4 +1,4 @@
-SUBDIRS = GAIL Gecko J2SE-access-bridge Qt WebKitGtk clutter gtk
+SUBDIRS = Chromium GAIL Gecko J2SE-access-bridge Qt WebKitGtk clutter gtk
 
 orca_python_PYTHON = \
        __init__.py \
diff --git a/src/orca/scripts/toolkits/__init__.py b/src/orca/scripts/toolkits/__init__.py
index 827c989d7..ccc4d839c 100644
--- a/src/orca/scripts/toolkits/__init__.py
+++ b/src/orca/scripts/toolkits/__init__.py
@@ -1,4 +1,5 @@
 __all__ = ['clutter',
+           'Chromium',
            'gtk',
            'GAIL',
            'Gecko',


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