[orca] Create new, uniform spellcheck support and implement for Gedit and Thunderbird



commit 4948d54f263fe58371fef8b9e04b1708fc2d033f
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Mon Feb 17 15:41:33 2014 -0500

    Create new, uniform spellcheck support and implement for Gedit and Thunderbird

 po/POTFILES.in                                  |    1 -
 src/orca/Makefile.am                            |    1 +
 src/orca/guilabels.py                           |   22 ++
 src/orca/script.py                              |    5 +
 src/orca/scripts/apps/Thunderbird/Makefile.am   |    3 +-
 src/orca/scripts/apps/Thunderbird/script.py     |  135 ++++++---
 src/orca/scripts/apps/Thunderbird/spellcheck.py |   76 +++++
 src/orca/scripts/apps/gedit/Makefile.am         |    3 +-
 src/orca/scripts/apps/gedit/script.py           |  353 +++++++----------------
 src/orca/scripts/apps/gedit/spellcheck.py       |   59 ++++
 src/orca/scripts/default.py                     |   10 +-
 src/orca/settings.py                            |    7 +
 src/orca/spellcheck.py                          |  296 +++++++++++++++++++
 13 files changed, 668 insertions(+), 303 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index d7ecc16..afd46a7 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -19,7 +19,6 @@ src/orca/object_properties.py
 [type: gettext/glade]src/orca/orca-setup.ui
 src/orca/phonnames.py
 src/orca/scripts/apps/evolution/speech_generator.py
-src/orca/scripts/apps/gedit/script.py
 src/orca/scripts/apps/gnome-mud/script.py
 src/orca/scripts/apps/liferea/script.py
 src/orca/scripts/apps/metacity/script.py
diff --git a/src/orca/Makefile.am b/src/orca/Makefile.am
index 3c195a1..5b7de18 100644
--- a/src/orca/Makefile.am
+++ b/src/orca/Makefile.am
@@ -61,6 +61,7 @@ orca_python_PYTHON = \
        settings_manager.py \
        sound.py \
        speech.py \
+       spellcheck.py \
        speechdispatcherfactory.py \
        speech_generator.py \
        speechserver.py \
diff --git a/src/orca/guilabels.py b/src/orca/guilabels.py
index c262a0e..3a34912 100644
--- a/src/orca/guilabels.py
+++ b/src/orca/guilabels.py
@@ -636,6 +636,28 @@ SPEECH_VOICE_TYPE_UPPERCASE = C_("VoiceType", "Uppercase")
 # system. (http://devel.freebsoft.org/speechd)
 SPEECH_DISPATCHER = _("Speech Dispatcher")
 
+# Translators: This is a label for a group of options related to Orca's behavior
+# when presenting an application's spell check dialog.
+SPELL_CHECK = C_("OptionGroup", "Spell Check")
+
+# Translators: This is a label for a checkbox associated with an Orca setting.
+# When this option is enabled, Orca will spell out the current error in addition
+# to speaking it. For example, if the misspelled word is "foo," enabling this
+# setting would cause Orca to speak "f o o" after speaking "foo".
+SPELL_CHECK_SPELL_ERROR = _("Spell _error")
+
+# Translators: This is a label for a checkbox associated with an Orca setting.
+# When this option is enabled, Orca will spell out the current suggestion in
+# addition to speaking it. For example, if the misspelled word is "foo," and
+# the first suggestion is "for" enabling this setting would cause Orca to speak
+# "f o r" after speaking "for".
+SPELL_CHECK_SPELL_SUGGESTION = _("Spell _suggestion")
+
+# Translators: This is a label for a checkbox associated with an Orca setting.
+# When this option is enabled, Orca will present the context (surrounding text,
+# typically the sentence or line) in which the mistake occurred.
+SPELL_CHECK_PRESENT_CONTEXT = _("Present _context of error")
+
 # Translators: This is a label for an option to tell Orca whether or not it
 # should speak the coordinates of the current spread sheet cell. Coordinates are
 # the row and column position within the spread sheet (i.e. A1, B1, C2 ...)
diff --git a/src/orca/script.py b/src/orca/script.py
index 927fd24..3d7488a 100644
--- a/src/orca/script.py
+++ b/src/orca/script.py
@@ -115,6 +115,7 @@ class Script:
         self.eventCache = {}
         self.whereAmI = self.getWhereAmI()
         self.bookmarks = self.getBookmarks()
+        self.spellcheck = self.getSpellCheck()
         self.voices = settings.voices
         self.tutorialGenerator = self.getTutorialGenerator()
 
@@ -228,6 +229,10 @@ class Script:
         """
         return None
 
+    def getSpellCheck(self):
+        """Returns the spellcheck support for this script."""
+        return None
+
     def getUtilities(self):
         """Returns the utilites for this script.
         """
diff --git a/src/orca/scripts/apps/Thunderbird/Makefile.am b/src/orca/scripts/apps/Thunderbird/Makefile.am
index db1d54d..1825bbd 100644
--- a/src/orca/scripts/apps/Thunderbird/Makefile.am
+++ b/src/orca/scripts/apps/Thunderbird/Makefile.am
@@ -4,7 +4,8 @@ orca_python_PYTHON = \
        script.py \
        script_settings.py \
        script_utilities.py \
-       speech_generator.py
+       speech_generator.py \
+       spellcheck.py
 
 orca_pythondir=$(pkgpythondir)/scripts/apps/Thunderbird
 
diff --git a/src/orca/scripts/apps/Thunderbird/script.py b/src/orca/scripts/apps/Thunderbird/script.py
index 6f42e55..ad85705 100644
--- a/src/orca/scripts/apps/Thunderbird/script.py
+++ b/src/orca/scripts/apps/Thunderbird/script.py
@@ -38,6 +38,7 @@ from orca.orca_i18n import _
 
 from .formatting import Formatting
 from .speech_generator import SpeechGenerator
+from .spellcheck import SpellCheck
 from .script_utilities import Utilities
 from . import script_settings
 
@@ -66,11 +67,6 @@ class Script(Gecko.Script):
 
         Gecko.Script.__init__(self, app)
 
-        # This will be used to cache a handle to the Thunderbird text area for
-        # spell checking purposes.
-
-        self.textArea = None
-
     def getFormatting(self):
         """Returns the formatting strings for this script."""
         return Formatting(self)
@@ -80,6 +76,11 @@ class Script(Gecko.Script):
 
         return SpeechGenerator(self)
 
+    def getSpellCheck(self):
+        """Returns the spellcheck support for this script."""
+
+        return SpellCheck(self)
+
     def getUtilities(self):
         """Returns the utilites for this script."""
 
@@ -95,6 +96,10 @@ class Script(Gecko.Script):
         #
         self.sayAllOnLoadCheckButton.set_active(script_settings.sayAllOnLoad)
 
+        spellcheck = self.spellcheck.getAppPreferencesGUI()
+        grid.attach(spellcheck, 0, len(grid.get_children()), 1, 1)
+        grid.show_all()
+
         return grid
 
     def setAppPreferences(self, prefs):
@@ -116,6 +121,17 @@ class Script(Gecko.Script):
         prefs.writelines("%s.sayAllOnLoad = %s\n" % (prefix, value))
         script_settings.sayAllOnLoad = value
 
+        self.spellcheck.setAppPreferences(prefs)
+
+    def doWhereAmI(self, inputEvent, basicOnly):
+        """Performs the whereAmI operation."""
+
+        if self.spellcheck.isActive():
+            self.spellcheck.presentErrorDetails(not basicOnly)
+            return
+
+        Gecko.Script.doWhereAmI(self,inputEvent, basicOnly)
+
     def onFocusedChanged(self, event):
         """Callback for object:state-changed:focused accessibility events."""
 
@@ -126,12 +142,18 @@ class Script(Gecko.Script):
         self.pointOfReference['lastAutoComplete'] = None
 
         obj = event.source
+        if self.spellcheck.isAutoFocusEvent(event):
+            orca.setLocusOfFocus(event, event.source, False)
+
+        if obj.parent == self.spellcheck.getSuggestionsList():
+            self.spellcheck.presentSuggestionListItem()
+            return
+
         if not self.inDocumentContent(obj):
             default.Script.onFocusedChanged(self, event)
             return
 
         if self.isEditableMessage(obj):
-            self.textArea = obj
             default.Script.onFocusedChanged(self, event)
             return
 
@@ -155,11 +177,39 @@ class Script(Gecko.Script):
                 self.speakMessage(obj.name)
                 self._presentMessage(obj)
 
+    def onCaretMoved(self, event):
+        """Callback for object:text-caret-moved accessibility events."""
+
+        if self.isEditableMessage(event.source):
+            if event.detail1 == -1:
+                return
+            self.spellcheck.setDocumentPosition(event.source, event.detail1)
+
+        Gecko.Script.onCaretMoved(self, event)
+
     def onChildrenChanged(self, event):
         """Callback for object:children-changed accessibility events."""
 
         default.Script.onChildrenChanged(self, event)
 
+    def onSelectionChanged(self, event):
+        """Callback for object:state-changed:showing accessibility events."""
+
+        # We present changes when the list has focus via focus-changed events.
+        if event.source == self.spellcheck.getSuggestionsList():
+            return
+
+        Gecko.Script.onSelectionChanged(self, event)
+
+    def onSensitiveChanged(self, event):
+        """Callback for object:state-changed:sensitive accessibility events."""
+
+        if event.source == self.spellcheck.getChangeToEntry() \
+           and self.spellcheck.presentCompletionMessage():
+            return
+
+        Gecko.Script.onSensitiveChanged(self, event)
+
     def onShowingChanged(self, event):
         """Callback for object:state-changed:showing accessibility events."""
 
@@ -204,6 +254,9 @@ class Script(Gecko.Script):
         if role == pyatspi.ROLE_LABEL and parentRole == pyatspi.ROLE_STATUS_BAR:
             return
 
+        if len(event.any_data) > 1 and obj == self.spellcheck.getChangeToEntry():
+            return
+
         isSystemEvent = event.type.endswith("system")
 
         # Try to stop unwanted chatter when a message is being replied to.
@@ -240,14 +293,26 @@ class Script(Gecko.Script):
     def onTextSelectionChanged(self, event):
         """Callback for object:text-selection-changed accessibility events."""
 
+        obj = event.source
+        spellCheckEntry = self.spellcheck.getChangeToEntry()
+        if obj == spellCheckEntry:
+            return
+
+        if self.isEditableMessage(obj) and self.spellcheck.isActive():
+            text = obj.queryText()
+            selStart, selEnd = text.getSelection(0)
+            self.spellcheck.setDocumentPosition(obj, selStart)
+            self.spellcheck.presentErrorDetails()
+            return
+
         default.Script.onTextSelectionChanged(self, event)
 
     def onNameChanged(self, event):
-        """Called whenever a property on an object changes.
+        """Callback for object:property-change:accessible-name events."""
 
-        Arguments:
-        - event: the Event
-        """
+        if event.source.name == self.spellcheck.getMisspelledWord():
+            self.spellcheck.presentErrorDetails()
+            return
 
         obj = event.source
 
@@ -269,39 +334,6 @@ class Script(Gecko.Script):
                 self.setCaretPosition(obj, offset)
                 return
 
-        # If we get a "object:property-change:accessible-name" event for 
-        # the first item in the Suggestions lists for the spell checking
-        # dialog, then speak the first two labels in that dialog. These
-        # will by the "Misspelled word:" label and the currently misspelled
-        # word. See bug #535192 for more details.
-        #
-        rolesList = [pyatspi.ROLE_LIST_ITEM,
-                     pyatspi.ROLE_LIST,
-                     pyatspi.ROLE_DIALOG,
-                     pyatspi.ROLE_APPLICATION]
-        if self.utilities.hasMatchingHierarchy(obj, rolesList):
-            dialog = obj.parent.parent
-
-            # Translators: this is what the name of the spell checking 
-            # dialog in Thunderbird begins with. The translated form
-            # has to match what Thunderbird is using.  We hate keying
-            # off stuff like this, but we're forced to do so in this case.
-            #
-            if dialog.name.startswith(_("Check Spelling")):
-                if obj.getIndexInParent() == 0:
-                    badWord = self.utilities.displayedText(dialog[1])
-
-                    if self.textArea != None:
-                        # If we have a handle to the Thunderbird message text
-                        # area, then extract out all the text objects, and
-                        # create a list of all the words found in them.
-                        #
-                        allTokens = []
-                        text = self.utilities.substring(self.textArea, 0, -1)
-                        tokens = text.split()
-                        allTokens += tokens
-                        self.speakMisspeltWord(allTokens, badWord)
-
     def _presentMessage(self, documentFrame):
         """Presents the first line of the message, or the entire message,
         depending on the user's sayAllOnLoad setting."""
@@ -383,3 +415,20 @@ class Script(Gecko.Script):
             return False
 
         return Gecko.Script.useCaretNavigationModel(self, keyboardEvent)
+
+    def onWindowActivated(self, event):
+        """Callback for window:activate accessibility events."""
+
+        Gecko.Script.onWindowActivated(self, event)
+        if not self.spellcheck.isCheckWindow(event.source):
+            return
+
+        self.spellcheck.presentErrorDetails()
+        orca.setLocusOfFocus(None, self.spellcheck.getChangeToEntry(), False)
+
+    def onWindowDeactivated(self, event):
+        """Callback for window:deactivate accessibility events."""
+
+        Gecko.Script.onWindowDeactivated(self, event)
+        if self.spellcheck.isCheckWindow(event.source):
+            self.spellcheck.deactivate()
diff --git a/src/orca/scripts/apps/Thunderbird/spellcheck.py b/src/orca/scripts/apps/Thunderbird/spellcheck.py
new file mode 100644
index 0000000..4099a32
--- /dev/null
+++ b/src/orca/scripts/apps/Thunderbird/spellcheck.py
@@ -0,0 +1,76 @@
+# Orca
+#
+# Copyright 2014 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.
+
+"""Customized support for spellcheck in Thunderbird."""
+
+__id__ = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (c) 2014 Igalia, S.L."
+__license__   = "LGPL"
+
+import pyatspi
+
+import orca.orca_state as orca_state
+import orca.spellcheck as spellcheck
+
+class SpellCheck(spellcheck.SpellCheck):
+
+    def __init__(self, script):
+        super(SpellCheck, self).__init__(script)
+
+    def isAutoFocusEvent(self, event):
+        if event.source != self._changeToEntry:
+            return False
+
+        locusOfFocus = orca_state.locusOfFocus
+        if not locusOfFocus:
+            return False
+
+        role = locusOfFocus.getRole()
+        if not role == pyatspi.ROLE_PUSH_BUTTON:
+            return False
+
+        lastKey, mods = self._script.utilities.lastKeyAndModifiers()
+        keys = self._script.utilities.mnemonicShortcutAccelerator(locusOfFocus)
+        for key in keys:
+            if key.endswith(lastKey.upper()):
+                return True
+
+        return False
+
+    def _isCandidateWindow(self, window):
+        return window and window.getRole() == pyatspi.ROLE_DIALOG
+
+    def _findChangeToEntry(self, root):
+        isEntry = lambda x: x and x.getRole() == pyatspi.ROLE_ENTRY \
+                  and x.getState().contains(pyatspi.STATE_SINGLE_LINE)
+        return pyatspi.findDescendant(root, isEntry)
+
+    def _findErrorWidget(self, root):
+        isError = lambda x: x and x.getRole() == pyatspi.ROLE_LABEL \
+                  and not ":" in x.name and not x.getRelationSet()
+        return pyatspi.findDescendant(root, isError)
+
+    def _findSuggestionsList(self, root):
+        isList = lambda x: x and x.getRole() == pyatspi.ROLE_LIST \
+                  and 'Selection' in x.get_interfaces()
+        return pyatspi.findDescendant(root, isList)
diff --git a/src/orca/scripts/apps/gedit/Makefile.am b/src/orca/scripts/apps/gedit/Makefile.am
index 2076568..4728a7d 100644
--- a/src/orca/scripts/apps/gedit/Makefile.am
+++ b/src/orca/scripts/apps/gedit/Makefile.am
@@ -1,6 +1,7 @@
 orca_python_PYTHON = \
        __init__.py \
-       script.py
+       script.py \
+       spellcheck.py
 
 orca_pythondir=$(pkgpythondir)/scripts/apps/gedit
 
diff --git a/src/orca/scripts/apps/gedit/script.py b/src/orca/scripts/apps/gedit/script.py
index ff2caa2..e76dc10 100644
--- a/src/orca/scripts/apps/gedit/script.py
+++ b/src/orca/scripts/apps/gedit/script.py
@@ -27,272 +27,98 @@ __license__   = "LGPL"
 
 import pyatspi
 
-import orca.debug as debug
+import orca.orca as orca
 import orca.orca_state as orca_state
 import orca.scripts.toolkits.gtk as gtk
-
-from orca.orca_i18n import _
+from .spellcheck import SpellCheck
 
 class Script(gtk.Script):
 
     def __init__(self, app):
-        """Creates a new script for the given application.
-
-        Arguments:
-        - app: the application to create a script for.
-        """
+        """Creates a new script for the given application."""
 
         gtk.Script.__init__(self, app)
 
-        # Set the debug level for all the methods in this script.
-        #
-        self.debugLevel = debug.LEVEL_FINEST
-
-        # This will be used to cache a handle to the gedit text area for
-        # spell checking purposes.
-
-        self.textArea = None
-
-        # The following variables will be used to try to determine if we've
-        # already handled this misspelt word (see readMisspeltWord() for
-        # more details.
-
-        self.lastCaretPosition = -1
-        self.lastBadWord = ''
-        self.lastEventType = ''
-
-    def readMisspeltWord(self, event, panel):
-        """Speak/braille the current misspelt word plus its context.
-           The spell check dialog contains a "paragraph" which shows the
-           context for the current spelling mistake. After speaking/brailling
-           the default action for this component, that a selection of the
-           surronding text from that paragraph with the misspelt word is also
-           spoken.
-
-        Arguments:
-        - event: the event.
-        - panel: the panel in the check spelling dialog containing the label
-                 with the misspelt word.
-        """
-
-        # Braille the default action for this component.
-        #
-        self.updateBraille(orca_state.locusOfFocus)
-
-        # Look for the label containing the misspelled word.
-        # There will be three labels in the top panel in the Check
-        # Spelling dialog. Look for the one that isn't a label to
-        # another component.
-        #
-        allLabels = self.utilities.descendantsWithRole(
-            panel, pyatspi.ROLE_LABEL)
-        for label in allLabels:
-            # Translators: these are labels from the gedit spell checking
-            # dialog and must be the same strings gedit uses.  We hate
-            # keying off stuff like this, but we're forced to do so in
-            # in this case.
-            #
-            if label.name.startswith(_("Change to:")) or \
-               label.name.startswith(_("Misspelled word:")):
-                continue
-            else:
-                badWord = label.name
-                break
-
-        # Note that we often get two or more of these focus or property-change
-        # events each time there is a new misspelt word. We extract the
-        # current text caret position and the misspelt word and compare
-        # them against the values saved from the last time this routine
-        # was called. If they are the same then we ignore it.
-
-        if self.textArea != None:
-            allText = self.utilities.descendantsWithRole(
-                self.textArea, pyatspi.ROLE_TEXT)
-            caretPosition = allText[0].queryText().caretOffset
-
-            debug.println(self.debugLevel, \
-                "gedit.readMisspeltWord: type=%s  word=%s caret position=%d" \
-                % (event.type, badWord, caretPosition))
-
-            if (caretPosition == self.lastCaretPosition) and \
-               (badWord == self.lastBadWord) and \
-               (event.type == self.lastEventType):
-                return
-
-            # The indication that spell checking is complete is when the
-            # "misspelt" word is set to "Completed spell checking". Ugh!
-            # Try to detect this and let the user know.
-            #
-            # Translators: this string must be the same that is used by
-            # gedit.  We hate keying off stuff like this, but we're
-            # forced to do so in this case.
-            #
-            if badWord == _("Completed spell checking"):
-                utterance = _("Spell checking is complete.")
-                self.presentMessage(utterance)
-                utterance = _("Press Tab and Return to terminate.")
-                self.presentMessage(utterance)
-                return
-
-            # If we have a handle to the gedit text area, then extract out
-            # all the text objects, and create a list of all the words found
-            # in them.
-            #
-            allTokens = []
-            for i in range(0, len(allText)):
-                text = self.utilities.substring(allText[i], 0, -1)
-                tokens = text.split()
-                allTokens += tokens
-
-            self.speakMisspeltWord(allTokens, badWord)
-
-            # Save misspelt word information for comparison purposes
-            # next time around.
-            #
-            self.lastCaretPosition = caretPosition
-            self.lastBadWord = badWord
-            self.lastEventType = event.type
-
-    def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus):
-        """Called when the visual object with focus changes.
-
-        Arguments:
-        - event: if not None, the Event that caused the change
-        - oldLocusOfFocus: Accessible that is the old locus of focus
-        - newLocusOfFocus: Accessible that is the new locus of focus
-        """
-
-        details = debug.getAccessibleDetails(self.debugLevel, event.source)
-        debug.printObjectEvent(self.debugLevel, event, details)
-
-        # 1) Text area (for caching handle for spell checking purposes).
-        #
-        # This works in conjunction with code in section 2). Check to see if
-        # focus is currently in the gedit text area. If it is, then, if this
-        # is the first time, save a pointer to the scroll pane which contains
-        # the text being editted.
-        #
-        # Note that this drops through to then use the default event
-        # processing in the parent class for this "focus:" event.
-
-        rolesList = [pyatspi.ROLE_TEXT,
-                     pyatspi.ROLE_SCROLL_PANE,
-                     pyatspi.ROLE_FILLER,
-                     pyatspi.ROLE_PAGE_TAB,
-                     pyatspi.ROLE_PAGE_TAB_LIST,
-                     pyatspi.ROLE_SPLIT_PANE]
-        if self.utilities.hasMatchingHierarchy(event.source, rolesList):
-            debug.println(self.debugLevel,
-                          "gedit.locusOfFocusChanged - text area.")
-
-            self.textArea = event.source.parent
-            # Fall-thru to process the event with the default handler.
-
-        # 2) check spelling dialog.
-        #
-        # Check to see if the Spell Check dialog has just appeared and got
-        # focus. If it has, then speak/braille the current misspelt word
-        # plus its context.
-        #
-        # Note that in order to make sure that this focus event is for the
-        # check spelling dialog, a check is made of the localized name of the
-        # option pane. Translators for other locales will need to ensure that
-        # their translation of this string matches what gedit uses in
-        # that locale.
-
-        rolesList = [pyatspi.ROLE_TEXT,
-                     pyatspi.ROLE_FILLER,
-                     pyatspi.ROLE_PANEL,
-                     pyatspi.ROLE_FILLER,
-                     pyatspi.ROLE_FRAME]
-        if self.utilities.hasMatchingHierarchy(event.source, rolesList):
-            tmp = event.source.parent.parent
-            frame = tmp.parent.parent
-            # Translators: this is the name of the "Check Spelling" window
-            # in gedit and must be the same as what gedit uses.  We hate
-            # keying off stuff like this, but we're forced to do so in this
-            # case.
-            #
-            if frame.name.startswith(_("Check Spelling")):
-                debug.println(self.debugLevel,
-                        "gedit.locusOfFocusChanged - check spelling dialog.")
-
-                self.readMisspeltWord(event, event.source.parent.parent)
-                # Fall-thru to process the event with the default handler.
-
-        # For everything else, pass the focus event onto the parent class
-        # to be handled in the default way.
-
-        gtk.Script.locusOfFocusChanged(self, event,
-                                           oldLocusOfFocus, newLocusOfFocus)
-
-        # If we are doing a Print Preview and we are focused on the
-        # page number text area, also speak the "of n" labels to the
-        # right of this area. See bug #133275 for more details.
-        #
-        rolesList = [pyatspi.ROLE_TEXT,
-                     pyatspi.ROLE_FILLER,
-                     pyatspi.ROLE_PANEL,
-                     pyatspi.ROLE_TOOL_BAR,
-                     pyatspi.ROLE_FILLER,
-                     pyatspi.ROLE_FILLER,
-                     pyatspi.ROLE_PAGE_TAB,
-                     pyatspi.ROLE_PAGE_TAB_LIST]
-        if self.utilities.hasMatchingHierarchy(event.source, rolesList):
-            parent = event.source.parent
-            label1 = self.utilities.displayedText(parent[1])
-            label2 = self.utilities.displayedText(parent[2])
-            items = [label1, label2]
-            self.presentItemsInSpeech(items)
-            self.presentItemsInBraille(items)
-
-    # This method tries to detect and handle the following cases:
-    # 1) check spelling dialog.
+    def getSpellCheck(self):
+        """Returns the spellcheck for this script."""
+
+        return SpellCheck(self)
+
+    def getAppPreferencesGUI(self):
+        """Returns a GtkGrid containing the application unique configuration
+        GUI items for the current application."""
+
+        from gi.repository import Gtk
+
+        grid = Gtk.Grid()
+        grid.set_border_width(12)
+        grid.attach(self.spellcheck.getAppPreferencesGUI(), 0, 0, 1, 1)
+        grid.show_all()
+
+        return grid
+
+    def setAppPreferences(self, prefs):
+        """Write out the application specific preferences lines and set the
+        new values."""
+
+        self.spellcheck.setAppPreferences(prefs)
+
+    def doWhereAmI(self, inputEvent, basicOnly):
+        """Performs the whereAmI operation."""
+
+        if self.spellcheck.isActive():
+            self.spellcheck.presentErrorDetails(not basicOnly)
+            return
+
+        gtk.Script.doWhereAmI(self,inputEvent, basicOnly)
+
+    def onActiveDescendantChanged(self, event):
+        """Callback for object:active-descendant-changed accessibility events."""
+
+        if event.source == self.spellcheck.getSuggestionsList():
+            return
+
+        gtk.Script.onActiveDescendantChanged(self, event)
+
+    def onCaretMoved(self, event):
+        """Callback for object:text-caret-moved accessibility events."""
+
+        state = event.source.getState()
+        if state.contains(pyatspi.STATE_MULTI_LINE):
+            self.spellcheck.setDocumentPosition(event.source, event.detail1)
+
+        gtk.Script.onCaretMoved(self, event)
+
+    def onFocusedChanged(self, event):
+        """Callback for object:state-changed:focused accessibility events."""
+
+        if not event.detail1:
+            return
+
+        if event.source.parent == self.spellcheck.getSuggestionsList():
+            self.spellcheck.presentSuggestionListItem()
+            return
+
+        gtk.Script.onFocusedChanged(self, event)
 
     def onNameChanged(self, event):
-        """Called whenever a property on an object changes.
-
-        Arguments:
-        - event: the Event
-        """
-
-        details = debug.getAccessibleDetails(self.debugLevel, event.source)
-        debug.printObjectEvent(self.debugLevel, event, details)
-
-        # 1) check spelling dialog.
-        #
-        # Check to see if if we've had a property-change event for the
-        # accessible name for the label containing the current misspelt
-        # word in the check spelling dialog.
-        # This (hopefully) means that the user has just corrected a
-        # spelling mistake, in which case, speak/braille the current
-        # misspelt word plus its context.
-        #
-        # Note that in order to make sure that this event is for the
-        # check spelling dialog, a check is made of the localized name of the
-        # frame. Translators for other locales will need to ensure that
-        # their translation of this string matches what gedit uses in
-        # that locale.
-
-        rolesList = [pyatspi.ROLE_LABEL,
-                     pyatspi.ROLE_PANEL,
-                     pyatspi.ROLE_FILLER,
-                     pyatspi.ROLE_FRAME]
-        if self.utilities.hasMatchingHierarchy(event.source, rolesList):
-            frame = event.source.parent.parent.parent
-            # Translators: this is the name of the "Check Spelling" window
-            # in gedit and must be the same as what gedit uses.  We hate
-            # keying off stuff like this, but we're forced to do so in this
-            # case.
-            #
-            if frame.name.startswith(_("Check Spelling")):
-                debug.println(self.debugLevel,
-                      "gedit.onNameChanged - check spelling dialog.")
-
-                self.readMisspeltWord(event, event.source.parent)
-                # Fall-thru to process the event with the default handler.
-
-        gtk.Script.onNameChanged(self, event)
+        """Callback for object:property-change:accessible-name events."""
+
+        if not self.spellcheck.isActive():
+            gtk.Script.onNameChanged(self, event)
+            return
+
+        if event.source.name == self.spellcheck.getMisspelledWord():
+            self.spellcheck.presentErrorDetails()
+
+    def onSensitiveChanged(self, event):
+        """Callback for object:state-changed:sensitive accessibility events."""
+
+        if event.source == self.spellcheck.getChangeToEntry() \
+           and self.spellcheck.presentCompletionMessage():
+            return
+
+        gtk.Script.onSensitiveChanged(self, event)
 
     def onTextSelectionChanged(self, event):
         """Callback for object:text-selection-changed accessibility events."""
@@ -310,3 +136,20 @@ class Script(gtk.Script):
             return
 
         self.sayLine(event.source)
+
+    def onWindowActivated(self, event):
+        """Callback for window:activate accessibility events."""
+
+        gtk.Script.onWindowActivated(self, event)
+        if not self.spellcheck.isCheckWindow(event.source):
+            return
+
+        self.spellcheck.presentErrorDetails()
+        orca.setLocusOfFocus(None, self.spellcheck.getChangeToEntry(), False)
+
+    def onWindowDeactivated(self, event):
+        """Callback for window:deactivate accessibility events."""
+
+        gtk.Script.onWindowDeactivated(self, event)
+        if self.spellcheck.isCheckWindow(event.source):
+            self.spellcheck.deactivate()
diff --git a/src/orca/scripts/apps/gedit/spellcheck.py b/src/orca/scripts/apps/gedit/spellcheck.py
new file mode 100644
index 0000000..342afed
--- /dev/null
+++ b/src/orca/scripts/apps/gedit/spellcheck.py
@@ -0,0 +1,59 @@
+# Orca
+#
+# Copyright 2014 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.
+
+"""Customized support for spellcheck in Gedit."""
+
+__id__ = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (c) 2014 Igalia, S.L."
+__license__   = "LGPL"
+
+import pyatspi
+import orca.spellcheck as spellcheck
+
+class SpellCheck(spellcheck.SpellCheck):
+
+    def __init__(self, script):
+        super(SpellCheck, self).__init__(script)
+
+    def _isCandidateWindow(self, window):
+        return window and window.getRole() == pyatspi.ROLE_FRAME
+
+    def _findChangeToEntry(self, root):
+        isEntry = lambda x: x and x.getRole() == pyatspi.ROLE_TEXT \
+                  and x.getState().contains(pyatspi.STATE_SINGLE_LINE)
+        return pyatspi.findDescendant(root, isEntry)
+
+    def _findErrorWidget(self, root):
+        isPanel = lambda x: x and x.getRole() == pyatspi.ROLE_PANEL
+        panel = pyatspi.findAncestor(self._changeToEntry, isPanel)
+        if not panel:
+            return None
+
+        isError = lambda x: x and x.getRole() == pyatspi.ROLE_LABEL \
+                  and not ":" in x.name and not x.getRelationSet()
+        return pyatspi.findDescendant(panel, isError)
+
+    def _findSuggestionsList(self, root):
+        isTable = lambda x: x and x.getRole() == pyatspi.ROLE_TABLE \
+                  and 'Selection' in x.get_interfaces()
+        return pyatspi.findDescendant(root, isTable)
diff --git a/src/orca/scripts/default.py b/src/orca/scripts/default.py
index 4468e23..a0ca272 100644
--- a/src/orca/scripts/default.py
+++ b/src/orca/scripts/default.py
@@ -549,6 +549,8 @@ class Script(script.Script):
             self.onExpandedChanged
         listeners["object:state-changed:selected"]          = \
             self.onSelectedChanged
+        listeners["object:state-changed:sensitive"]         = \
+            self.onSensitiveChanged
         listeners["object:text-attributes-changed"]         = \
             self.onTextAttributesChanged
         listeners["object:text-selection-changed"]          = \
@@ -1363,10 +1365,10 @@ class Script(script.Script):
 
         for (charIndex, character) in enumerate(itemString):
             if character.isupper():
-                speech.speak(character,
+                speech.speakCharacter(character,
                              self.voices[settings.UPPERCASE_VOICE])
             else:
-                speech.speak(character)
+                speech.speakCharacter(character)
 
     def _reviewCurrentItem(self, inputEvent, targetCursorCell=0,
                            speechType=1):
@@ -2405,6 +2407,10 @@ class Script(script.Script):
                 orca.setLocusOfFocus(event, child)
                 break
 
+    def onSensitiveChanged(self, event):
+        """Callback for object:state-changed:sensitive accessibility events."""
+        pass
+
     def onFocus(self, event):
         """Callback for focus: accessibility events."""
 
diff --git a/src/orca/settings.py b/src/orca/settings.py
index b1778a8..466f0dd 100644
--- a/src/orca/settings.py
+++ b/src/orca/settings.py
@@ -109,6 +109,9 @@ userCustomizableSettings = [
     "presentTimeFormat",
     "activeProfile",
     "startingProfile",
+    "spellcheckSpellError",
+    "spellcheckSpellSuggestion",
+    "spellcheckPresentContext",
 ]
 
 excludeKeys = ["pronunciations",
@@ -823,3 +826,7 @@ presentDateFormat = DATE_FORMAT_LOCALE
 
 # Default tty to pass along to brlapi.
 tty = 7
+
+spellcheckSpellError = True
+spellcheckSpellSuggestion = True
+spellcheckPresentContext = True
diff --git a/src/orca/spellcheck.py b/src/orca/spellcheck.py
new file mode 100644
index 0000000..962303a
--- /dev/null
+++ b/src/orca/spellcheck.py
@@ -0,0 +1,296 @@
+# Orca
+#
+# Copyright 2014 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.
+
+"""Script-customizable support for application spellcheckers."""
+
+__id__ = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (c) 2014 Igalia, S.L."
+__license__   = "LGPL"
+
+import pyatspi
+import re
+
+from orca import guilabels
+from orca import messages
+from orca import settings_manager
+
+_settingsManager = settings_manager.getManager()
+
+class SpellCheck:
+
+    def __init__(self, script, hasChangeToEntry=True):
+        self._script = script
+        self._hasChangeToEntry = hasChangeToEntry
+        self._clearState()
+
+        self.spellErrorCheckButton = None
+        self.spellSuggestionCheckButton = None
+        self.presentContextCheckButton = None
+
+    def activate(self, window):
+        if not self._isCandidateWindow(window):
+            return False
+
+        if self._hasChangeToEntry:
+            self._changeToEntry = self._findChangeToEntry(window)
+            if not self._changeToEntry:
+                return False
+
+        self._errorWidget = self._findErrorWidget(window)
+        if not self._errorWidget:
+            return False
+
+        self._suggestionsList = self._findSuggestionsList(window)
+        if not self._suggestionsList:
+            return False
+
+        self._window = window
+        self._activated = True
+        return True
+
+    def deactivate(self):
+        self._clearState()
+
+    def getDocumentPosition(self):
+        return self._documentPosition
+
+    def setDocumentPosition(self, obj, offset):
+        self._documentPosition = obj, offset
+
+    def getErrorWidget(self):
+        return self._errorWidget
+
+    def getMisspelledWord(self):
+        if not self._errorWidget:
+            return ""
+
+        return self._script.utilities.displayedText(self._errorWidget)
+
+    def getCompletionMessage(self):
+        if not self._errorWidget:
+            return ""
+
+        return self._script.utilities.displayedText(self._errorWidget)
+
+    def getChangeToEntry(self):
+        return self._changeToEntry
+
+    def getSuggestionsList(self):
+        return self._suggestionsList
+
+    def isActive(self):
+        return self._activated
+
+    def isCheckWindow(self, window):
+        if window and window == self._window:
+            return True
+
+        return self.activate(window)
+
+    def isComplete(self):
+        try:
+            state = self._changeToEntry.getState()
+        except:
+            return False
+        return not state.contains(pyatspi.STATE_SENSITIVE)
+
+    def isAutoFocusEvent(self, event):
+        return False
+
+    def presentContext(self):
+        if not self.isActive():
+            return False
+
+        obj, offset = self._documentPosition
+        if not (obj and offset >= 0):
+            return False
+
+        try:
+            text = obj.queryText()
+        except:
+            return False
+
+        # This should work, but some toolkits are broken.
+        boundary = pyatspi.TEXT_BOUNDARY_SENTENCE_START
+        string, start, end = text.getTextAtOffset(offset, boundary)
+
+        if not string:
+            boundary = pyatspi.TEXT_BOUNDARY_LINE_START
+            string, start, end = text.getTextAtOffset(offset, boundary)
+            sentences = re.split(r'(?:\.|\!|\?)', string)
+            word = self.getMisspelledWord()
+            if string.count(word) == 1:
+                match = list(filter(lambda x: x.count(word), sentences))
+                string = match[0]
+
+        if not string:
+            return False
+
+        self._script.speakMessage(messages.MISSPELLED_WORD_CONTEXT % string)
+        return True
+
+    def presentCompletionMessage(self):
+        if not (self.isActive() and self.isComplete()):
+            return False
+
+        self._script.presentMessage(self.getCompletionMessage())
+        return True
+
+    def presentErrorDetails(self, detailed=False):
+        if self.isComplete():
+            return False
+
+        if self.presentMistake(detailed):
+            self.presentSuggestion(detailed)
+            if detailed or _settingsManager.getSetting('spellcheckPresentContext'):
+                self.presentContext()
+            return True
+
+        return False
+
+    def presentMistake(self, detailed=False):
+        if not self.isActive():
+            return False
+
+        word = self.getMisspelledWord()
+        if not word:
+            return False
+
+        self._script.speakMessage(messages.MISSPELLED_WORD % word)
+        if detailed or _settingsManager.getSetting('spellcheckSpellError'):
+            self._script.spellCurrentItem(word)
+
+        return True
+
+    def presentSuggestion(self, detailed=False):
+        if not self._hasChangeToEntry:
+            return self.presentSuggestionListItem(detailed)
+
+        if not self.isActive():
+            return False
+
+        entry = self._changeToEntry
+        if not entry:
+            return False
+
+        label = self._script.utilities.displayedLabel(entry)
+        string = self._script.utilities.substring(entry, 0, -1)
+        self._script.speakMessage("%s %s" % (label, string))
+        if detailed or _settingsManager.getSetting('spellcheckSpellSuggestion'):
+            self._script.spellCurrentItem(string)
+
+        return True
+
+    def presentSuggestionListItem(self, detailed=False):
+        if not self.isActive():
+            return False
+
+        suggestions = self._suggestionsList
+        if not suggestions:
+            return False
+
+        items = self._script.utilities.selectedChildren(suggestions)
+        if not len(items) == 1:
+            return False
+
+        string = items[0].name
+        self._script.speakMessage(string)
+        if detailed or _settingsManager.getSetting('spellcheckSpellSuggestion'):
+            self._script.spellCurrentItem(string)
+
+        return True
+
+    def _clearState(self):
+        self._window = None
+        self._errorWidget = None
+        self._changeToEntry = None
+        self._suggestionsList = None
+        self._activated = False
+        self._documentPosition = None, -1
+
+    def _isCandidateWindow(self, window):
+        return False
+
+    def _findChangeToEntry(self, root):
+        return None
+
+    def _findErrorWidget(self, root):
+        return None
+
+    def _findSuggestionsList(self, root):
+        return None
+
+    def getAppPreferencesGUI(self):
+
+        from gi.repository import Gtk
+
+        frame = Gtk.Frame()
+        label = Gtk.Label(label="<b>%s</b>" % guilabels.SPELL_CHECK)
+        label.set_use_markup(True)
+        frame.set_label_widget(label)
+
+        alignment = Gtk.Alignment.new(0.5, 0.5, 1, 1)
+        alignment.set_padding(0, 0, 12, 0)
+        frame.add(alignment)
+
+        grid = Gtk.Grid()
+        alignment.add(grid)
+
+        label = guilabels.SPELL_CHECK_SPELL_ERROR
+        value = _settingsManager.getSetting('spellcheckSpellError')
+        self.spellErrorCheckButton = Gtk.CheckButton.new_with_mnemonic(label)
+        self.spellErrorCheckButton.set_active(value)
+        grid.attach(self.spellErrorCheckButton, 0, 0, 1, 1)
+
+        label = guilabels.SPELL_CHECK_SPELL_SUGGESTION
+        value = _settingsManager.getSetting('spellcheckSpellSuggestion')
+        self.spellSuggestionCheckButton = Gtk.CheckButton.new_with_mnemonic(label)
+        self.spellSuggestionCheckButton.set_active(value)
+        grid.attach(self.spellSuggestionCheckButton, 0, 1, 1, 1)
+
+        label = guilabels.SPELL_CHECK_PRESENT_CONTEXT
+        value = _settingsManager.getSetting('spellcheckPresentContext')
+        self.presentContextCheckButton = Gtk.CheckButton.new_with_mnemonic(label)
+        self.presentContextCheckButton.set_active(value)
+        grid.attach(self.presentContextCheckButton, 0, 2, 1, 1)
+
+        return frame
+
+    def setAppPreferences(self, prefs):
+
+        prefix = "orca.settings"
+
+        value = self.spellErrorCheckButton.get_active()
+        _settingsManager.setSetting('spellcheckSpellError', value)
+        prefs.writelines("\n")
+        prefs.writelines("%s.spellcheckSpellError = %s\n" % (prefix, value))
+
+        value = self.spellSuggestionCheckButton.get_active()
+        _settingsManager.setSetting('spellcheckSpellSuggestion', value)
+        prefs.writelines("\n")
+        prefs.writelines("%s.spellcheckSpellSuggestion = %s\n" % (prefix, value))
+
+        value = self.presentContextCheckButton.get_active()
+        _settingsManager.setSetting('spellcheckPresentContext', value)
+        prefs.writelines("\n")
+        prefs.writelines("%s.spellcheckPresentContext = %s\n" % (prefix, value))



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