[orca] Use Orca's spellcheck support in the LibreOffice script



commit 7c5871e4613558e5e6a05b86df205270506296b9
Author: Joanmarie Diggs <jdiggs igalia com>
Date:   Fri Aug 14 19:23:36 2015 -0400

    Use Orca's spellcheck support in the LibreOffice script
    
    Note: This support is partially blocked by the following LibreOffice bugs:
    * https://bugs.documentfoundation.org/show_bug.cgi?id=93430
    * https://bugs.documentfoundation.org/show_bug.cgi?id=86661
    
    In order to make the first one less of an annoyance, users are encouraged
    to uncheck the 'Spell error' and 'Present context of error' checkboxes in
    Orca's preferences for LibreOffice.

 src/orca/scripts/apps/soffice/Makefile.am         |    1 +
 src/orca/scripts/apps/soffice/script.py           |  203 +++++----------------
 src/orca/scripts/apps/soffice/speech_generator.py |    3 +
 src/orca/scripts/apps/soffice/spellcheck.py       |   97 ++++++++++
 src/orca/spellcheck.py                            |   10 +-
 5 files changed, 155 insertions(+), 159 deletions(-)
---
diff --git a/src/orca/scripts/apps/soffice/Makefile.am b/src/orca/scripts/apps/soffice/Makefile.am
index 918701e..9ddc441 100644
--- a/src/orca/scripts/apps/soffice/Makefile.am
+++ b/src/orca/scripts/apps/soffice/Makefile.am
@@ -5,6 +5,7 @@ orca_python_PYTHON = \
        script.py \
        script_utilities.py \
        speech_generator.py \
+       spellcheck.py \
        structural_navigation.py
 
 orca_pythondir=$(pkgpythondir)/scripts/apps/soffice
diff --git a/src/orca/scripts/apps/soffice/script.py b/src/orca/scripts/apps/soffice/script.py
index 5e9d3f0..0e565f4 100644
--- a/src/orca/scripts/apps/soffice/script.py
+++ b/src/orca/scripts/apps/soffice/script.py
@@ -43,11 +43,12 @@ import orca.speech as speech
 import orca.settings as settings
 import orca.settings_manager as settings_manager
 
-from .speech_generator import SpeechGenerator
 from .braille_generator import BrailleGenerator
 from .formatting import Formatting
-from .structural_navigation import StructuralNavigation
 from .script_utilities import Utilities
+from .spellcheck import SpellCheck
+from .speech_generator import SpeechGenerator
+from .structural_navigation import StructuralNavigation
 
 _settingsManager = settings_manager.getManager()
 
@@ -77,15 +78,6 @@ class Script(default.Script):
         self.dynamicColumnHeaders = {}
         self.dynamicRowHeaders = {}
 
-        # The following variables will be used to try to determine if we've
-        # already handled this misspelt word (see readMisspeltWord() for
-        # more details.
-
-        self.lastTextLength = -1
-        self.lastBadWord = ''
-        self.lastStartOff = -1
-        self.lastEndOff = -1
-
     def getBrailleGenerator(self):
         """Returns the braille generator for this script.
         """
@@ -96,6 +88,11 @@ class Script(default.Script):
         """
         return SpeechGenerator(self)
 
+    def getSpellCheck(self):
+        """Returns the spellcheck for this script."""
+
+        return SpellCheck(self)
+
     def getFormatting(self):
         """Returns the formatting strings for this script."""
         return Formatting(self)
@@ -273,6 +270,8 @@ class Script(default.Script):
         self.skipBlankCellsCheckButton.set_active(value)
         tableGrid.attach(self.skipBlankCellsCheckButton, 0, 3, 1, 1)
 
+        spellcheck = self.spellcheck.getAppPreferencesGUI()
+        grid.attach(spellcheck, 0, len(grid.get_children()), 1, 1)
         grid.show_all()
 
         return grid
@@ -280,7 +279,7 @@ class Script(default.Script):
     def getPreferencesFromGUI(self):
         """Returns a dictionary with the app-specific preferences."""
 
-        return {
+        prefs = {
             'speakCellSpan': self.speakCellSpanCheckButton.get_active(),
             'speakCellHeaders': self.speakCellHeadersCheckButton.get_active(),
             'skipBlankCells': self.skipBlankCellsCheckButton.get_active(),
@@ -288,6 +287,9 @@ class Script(default.Script):
             'speakSpreadsheetCoordinates': self.speakSpreadsheetCoordinatesCheckButton.get_active(),
         }
 
+        prefs.update(self.spellcheck.getPreferencesFromGUI())
+        return prefs
+
     def isStructuralNavigationCommand(self, inputEvent=None):
         """Checks to see if the inputEvent was a structural navigation
         command. This is necessary to prevent double-presentation of
@@ -309,6 +311,15 @@ class Script(default.Script):
 
         return False
 
+    def doWhereAmI(self, inputEvent, basicOnly):
+        """Performs the whereAmI operation."""
+
+        if self.spellcheck.isActive():
+            self.spellcheck.presentErrorDetails(not basicOnly)
+            return
+
+        super().doWhereAmI(inputEvent, basicOnly)
+
     def panBrailleLeft(self, inputEvent=None, panAmount=0):
         """In document content, we want to use the panning keys to browse the
         entire document.
@@ -506,114 +517,6 @@ class Script(default.Script):
 
         return True
 
-    def readMisspeltWord(self, event, pane):
-        """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.
-        - pane: the option pane in the spell check dialog.
-
-        Returns True if this is the spell check dialog (whether we actually
-        wind up reading the word or not).
-        """
-
-        def isMatch(obj):
-            if not (obj and obj.getRole() == pyatspi.ROLE_PARAGRAPH):
-                return False
-
-            if not obj.getState().contains(pyatspi.STATE_EDITABLE):
-                return False
-
-            try:
-                text = obj.queryText()
-            except:
-                return False
-
-            return text.characterCount > 0
-
-        paragraph = pyatspi.findAllDescendants(pane, isMatch)
-
-        # If there is not exactly one paragraph, this isn't the spellcheck
-        # dialog.
-        #
-        if len(paragraph) != 1:
-            return False
-
-        # If there's not any text displayed in the paragraph, this isn't
-        # the spellcheck dialog.
-        #
-        try:
-            text = paragraph[0].queryText()
-        except:
-            return False
-        else:
-            textLength = text.characterCount
-            if not textLength:
-                return False
-
-        # Determine which word is the misspelt word. This word will have
-        # non-default text attributes associated with it.
-        #
-        startFound = False
-        startOff = 0
-        endOff = textLength
-        for i in range(0, textLength):
-            attributes = text.getAttributes(i)
-            if len(attributes[0]) != 0:
-                if not startFound:
-                    startOff = i
-                    startFound = True
-            else:
-                if startFound:
-                    endOff = i
-                    break
-
-        if not startFound:
-            # If there are no text attributes in this paragraph, this isn't
-            # the spellcheck dialog.
-            #
-            return False
-
-        badWord = self.utilities.substring(paragraph[0], startOff, endOff - 1)
-
-        # 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
-        # length of the line of text, the misspelt word, the start and end
-        # offsets for that 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.
-        #
-        debug.println(debug.LEVEL_INFO,
-            "StarOffice.readMisspeltWord: type=%s  word=%s(%d,%d)  len=%d" % \
-            (event.type, badWord, startOff, endOff, textLength))
-
-        if (textLength == self.lastTextLength) and \
-           (badWord == self.lastBadWord) and \
-           (startOff == self.lastStartOff) and \
-           (endOff == self.lastEndOff):
-            return True
-
-        # Create a list of all the words found in the misspelt paragraph.
-        #
-        text = self.utilities.substring(paragraph[0], 0, -1)
-        allTokens = text.split()
-        self.speakMisspeltWord(allTokens, badWord)
-
-        # Save misspelt word information for comparison purposes next
-        # time around.
-        #
-        self.lastTextLength = textLength
-        self.lastBadWord = badWord
-        self.lastStartOff = startOff
-        self.lastEndOff = endOff
-
-        return True
-
     def locusOfFocusChanged(self, event, oldLocusOfFocus, newLocusOfFocus):
         """Called when the visual object with focus changes.
 
@@ -694,29 +597,6 @@ class Script(default.Script):
             self.pointOfReference['lastRow'] = row
             self.pointOfReference['lastColumn'] = column
 
-    def onWindowActivated(self, event):
-        """Called whenever a property on an object changes.
-
-        Arguments:
-        - event: the Event
-        """
-
-        self.lastTextLength = -1
-        self.lastBadWord = ''
-        self.lastStartOff = -1
-        self.lastEndOff = -1
-
-        default.Script.onWindowActivated(self, event)
-
-        # Maybe it's the spellcheck dialog. Might as well try and see.
-        # If it is, we want to speak the misspelled word and context
-        # after we've spoken the window name.
-        if event.source \
-           and event.source.getRole() == pyatspi.ROLE_DIALOG \
-           and event.source.childCount \
-           and event.source[0].getRole() == pyatspi.ROLE_OPTION_PANE:
-            self.readMisspeltWord(event, event.source)
-
     def onNameChanged(self, event):
         """Called whenever a property on an object changes.
 
@@ -724,19 +604,8 @@ class Script(default.Script):
         - event: the Event
         """
 
-        # Check to see if if we've had a property-change event for the
-        # accessible name for the option pane in the spell check 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.
-        #
-        rolesList = [pyatspi.ROLE_OPTION_PANE, \
-                     pyatspi.ROLE_DIALOG, \
-                     pyatspi.ROLE_APPLICATION]
-        if self.utilities.hasMatchingHierarchy(event.source, rolesList) \
-           and self.utilities.isSameObject(
-                event.source.parent, orca_state.activeWindow):
-            self.readMisspeltWord(event, event.source)
+        if self.spellcheck.isCheckWindow(event.source):
+            return
 
         # Impress slide navigation.
         #
@@ -772,6 +641,13 @@ class Script(default.Script):
         - event: the Event
         """
 
+        if event.source == self.spellcheck.getSuggestionsList():
+            if self.spellcheck.isSuggestionsItem(orca_state.locusOfFocus):
+                self.spellcheck.presentSuggestionListItem()
+            else:
+                self.spellcheck.presentErrorDetails()
+            return
+
         if self.utilities.isSameObject(event.any_data, orca_state.locusOfFocus):
             return
 
@@ -1014,3 +890,18 @@ class Script(default.Script):
             textLine[0] = self.utilities.displayedText(obj)
 
         return textLine
+
+    def onWindowActivated(self, event):
+        """Callback for window:activate accessibility events."""
+
+        super().onWindowActivated(event)
+        if not self.spellcheck.isCheckWindow(event.source):
+            return
+
+        self.spellcheck.presentErrorDetails()
+
+    def onWindowDeactivated(self, event):
+        """Callback for window:deactivate accessibility events."""
+
+        super().onWindowDeactivated(event)
+        self.spellcheck.deactivate()
diff --git a/src/orca/scripts/apps/soffice/speech_generator.py 
b/src/orca/scripts/apps/soffice/speech_generator.py
index 5a50bd8..8636df5 100644
--- a/src/orca/scripts/apps/soffice/speech_generator.py
+++ b/src/orca/scripts/apps/soffice/speech_generator.py
@@ -432,6 +432,9 @@ class SpeechGenerator(speech_generator.SpeechGenerator):
         if not priorObj or priorObj.getRoleName() == 'text frame':
             return []
 
+        if self._script.spellcheck.isActive():
+            return []
+
         return speech_generator.SpeechGenerator._generateNewAncestors(
             self, obj, **args)
 
diff --git a/src/orca/scripts/apps/soffice/spellcheck.py b/src/orca/scripts/apps/soffice/spellcheck.py
new file mode 100644
index 0000000..72bd903
--- /dev/null
+++ b/src/orca/scripts/apps/soffice/spellcheck.py
@@ -0,0 +1,97 @@
+# Orca
+#
+# Copyright 2015 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 LibreOffice."""
+
+__id__ = "$Id$"
+__version__   = "$Revision$"
+__date__      = "$Date$"
+__copyright__ = "Copyright (c) 2015 Igalia, S.L."
+__license__   = "LGPL"
+
+import pyatspi
+
+from orca import debug
+from orca import messages
+from orca import settings
+from orca import spellcheck
+
+
+class SpellCheck(spellcheck.SpellCheck):
+
+    def __init__(self, script):
+        super().__init__(script, hasChangeToEntry=False)
+
+    def _isCandidateWindow(self, window):
+        if window and window.childCount and window.getRole() == pyatspi.ROLE_FRAME:
+            child = window[0]
+            if child.getRole() == pyatspi.ROLE_DIALOG:
+                isPageTabList = lambda x: x and x.getRole() == pyatspi.ROLE_PAGE_TAB_LIST
+                if not pyatspi.findDescendant(child, isPageTabList):
+                    return True
+
+        return False
+
+    def _findErrorWidget(self, root):
+        isError = lambda x: x and x.getRole() == pyatspi.ROLE_TEXT and x.name \
+                  and x.parent.getRole() != pyatspi.ROLE_COMBO_BOX
+        return pyatspi.findDescendant(root, isError)
+
+    def _findSuggestionsList(self, root):
+        isList = lambda x: x and x.getRole() == pyatspi.ROLE_LIST and x.name \
+                  and 'Selection' in x.get_interfaces() \
+                  and x.parent.getRole() != pyatspi.ROLE_COMBO_BOX
+        return pyatspi.findDescendant(root, isList)
+
+    def getMisspelledWord(self):
+        try:
+            text = self._errorWidget.queryText()
+        except:
+            return ""
+
+        for i in range(text.characterCount):
+            attributes, start, end = text.getAttributeRun(i, False)
+            if attributes and start != end:
+                string = text.getText(start, end)
+                break
+        else:
+            msg = "INFO: No text attributes for word in %s." % self._errorWidget
+            debug.println(debug.LEVEL_INFO, msg)
+            string = text.getText(0, -1)
+
+        return string
+
+    def presentContext(self):
+        if not self.isActive():
+            return False
+
+        try:
+            text = self._errorWidget.queryText()
+        except:
+            return False
+
+        string = text.getText(0, -1)
+        if not string:
+            return False
+
+        voice = self._script.voices.get(settings.DEFAULT_VOICE)
+        self._script.speakMessage(messages.MISSPELLED_WORD_CONTEXT % string, voice=voice)
+        return True
diff --git a/src/orca/spellcheck.py b/src/orca/spellcheck.py
index 54816ac..5a3bc3c 100644
--- a/src/orca/spellcheck.py
+++ b/src/orca/spellcheck.py
@@ -197,7 +197,7 @@ class SpellCheck:
 
     def presentSuggestion(self, detailed=False):
         if not self._hasChangeToEntry:
-            return self.presentSuggestionListItem(detailed)
+            return self.presentSuggestionListItem(detailed, includeLabel=True)
 
         if not self.isActive():
             return False
@@ -215,7 +215,7 @@ class SpellCheck:
 
         return True
 
-    def presentSuggestionListItem(self, detailed=False):
+    def presentSuggestionListItem(self, detailed=False, includeLabel=False):
         if not self.isActive():
             return False
 
@@ -227,9 +227,13 @@ class SpellCheck:
         if not len(items) == 1:
             return False
 
+        if includeLabel:
+            label = self._script.utilities.displayedLabel(suggestions) or suggestions.name
+        else:
+            label = ""
         string = items[0].name
         voice = self._script.voices.get(settings.DEFAULT_VOICE)
-        self._script.speakMessage(string, voice=voice)
+        self._script.speakMessage(("%s %s" % (label, string)).strip(), voice=voice)
         if detailed or _settingsManager.getSetting('spellcheckSpellSuggestion'):
             self._script.spellCurrentItem(string)
 


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