1# Orca
2#
3# Copyright 2014 Igalia, S.L.
4#
5# Author: Joanmarie Diggs <jdiggs@igalia.com>
6#
7# This library is free software; you can redistribute it and/or
8# modify it under the terms of the GNU Lesser General Public
9# License as published by the Free Software Foundation; either
10# version 2.1 of the License, or (at your option) any later version.
11#
12# This library is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15# Lesser General Public License for more details.
16#
17# You should have received a copy of the GNU Lesser General Public
18# License along with this library; if not, write to the
19# Free Software Foundation, Inc., Franklin Street, Fifth Floor,
20# Boston MA  02110-1301 USA.
21
22"""Script-customizable support for application spellcheckers."""
23
24__id__ = "$Id$"
25__version__   = "$Revision$"
26__date__      = "$Date$"
27__copyright__ = "Copyright (c) 2014 Igalia, S.L."
28__license__   = "LGPL"
29
30import pyatspi
31import re
32
33from orca import guilabels
34from orca import messages
35from orca import object_properties
36from orca import orca_state
37from orca import settings_manager
38
39_settingsManager = settings_manager.getManager()
40
41class SpellCheck:
42
43    def __init__(self, script, hasChangeToEntry=True):
44        self._script = script
45        self._hasChangeToEntry = hasChangeToEntry
46        self._window = None
47        self._errorWidget = None
48        self._changeToEntry = None
49        self._suggestionsList = None
50        self._activated = False
51        self._documentPosition = None, -1
52
53        self.spellErrorCheckButton = None
54        self.spellSuggestionCheckButton = None
55        self.presentContextCheckButton = None
56
57    def activate(self, window):
58        if not self._isCandidateWindow(window):
59            return False
60
61        if self._hasChangeToEntry:
62            self._changeToEntry = self._findChangeToEntry(window)
63            if not self._changeToEntry:
64                return False
65
66        self._errorWidget = self._findErrorWidget(window)
67        if not self._errorWidget:
68            return False
69
70        self._suggestionsList = self._findSuggestionsList(window)
71        if not self._suggestionsList:
72            return False
73
74        self._window = window
75        self._activated = True
76        return True
77
78    def deactivate(self):
79        self._clearState()
80
81    def getDocumentPosition(self):
82        return self._documentPosition
83
84    def setDocumentPosition(self, obj, offset):
85        self._documentPosition = obj, offset
86
87    def getErrorWidget(self):
88        return self._errorWidget
89
90    def getMisspelledWord(self):
91        if not self._errorWidget:
92            return ""
93
94        return self._script.utilities.displayedText(self._errorWidget)
95
96    def getCompletionMessage(self):
97        if not self._errorWidget:
98            return ""
99
100        return self._script.utilities.displayedText(self._errorWidget)
101
102    def getChangeToEntry(self):
103        return self._changeToEntry
104
105    def getSuggestionsList(self):
106        return self._suggestionsList
107
108    def isActive(self):
109        return self._activated
110
111    def isCheckWindow(self, window):
112        if window and window == self._window:
113            return True
114
115        return self.activate(window)
116
117    def isComplete(self):
118        try:
119            state = self._changeToEntry.getState()
120        except:
121            return False
122        return not state.contains(pyatspi.STATE_SENSITIVE)
123
124    def isAutoFocusEvent(self, event):
125        return False
126
127    def isSuggestionsItem(self, obj):
128        if not self._suggestionsList:
129            return False
130
131        return obj and obj.parent == self._suggestionsList
132
133    def presentContext(self):
134        if not self.isActive():
135            return False
136
137        obj, offset = self._documentPosition
138        if not (obj and offset >= 0):
139            return False
140
141        try:
142            text = obj.queryText()
143        except:
144            return False
145
146        # This should work, but some toolkits are broken.
147        boundary = pyatspi.TEXT_BOUNDARY_SENTENCE_START
148        string, start, end = text.getTextAtOffset(offset, boundary)
149
150        if not string:
151            boundary = pyatspi.TEXT_BOUNDARY_LINE_START
152            string, start, end = text.getTextAtOffset(offset, boundary)
153            sentences = re.split(r'(?:\.|\!|\?)', string)
154            word = self.getMisspelledWord()
155            if string.count(word) == 1:
156                match = list(filter(lambda x: x.count(word), sentences))
157                string = match[0]
158
159        if not string:
160            return False
161
162        msg = messages.MISSPELLED_WORD_CONTEXT % string
163        voice = self._script.speechGenerator.voice(string=msg)
164        self._script.speakMessage(msg, voice=voice)
165        return True
166
167    def presentCompletionMessage(self):
168        if not (self.isActive() and self.isComplete()):
169            return False
170
171        self._script.clearBraille()
172        msg = self.getCompletionMessage()
173        voice = self._script.speechGenerator.voice(string=msg)
174        self._script.presentMessage(msg, voice=voice)
175        return True
176
177    def presentErrorDetails(self, detailed=False):
178        if self.isComplete():
179            return False
180
181        if self.presentMistake(detailed):
182            self.presentSuggestion(detailed)
183            if detailed or _settingsManager.getSetting('spellcheckPresentContext'):
184                self.presentContext()
185            return True
186
187        return False
188
189    def presentMistake(self, detailed=False):
190        if not self.isActive():
191            return False
192
193        word = self.getMisspelledWord()
194        if not word:
195            return False
196
197        msg = messages.MISSPELLED_WORD % word
198        voice = self._script.speechGenerator.voice(string=msg)
199        self._script.speakMessage(msg, voice=voice)
200        if detailed or _settingsManager.getSetting('spellcheckSpellError'):
201            self._script.spellCurrentItem(word)
202
203        return True
204
205    def presentSuggestion(self, detailed=False):
206        if not self._hasChangeToEntry:
207            return self.presentSuggestionListItem(detailed, includeLabel=True)
208
209        if not self.isActive():
210            return False
211
212        entry = self._changeToEntry
213        if not entry:
214            return False
215
216        label = self._script.utilities.displayedLabel(entry) or entry.name
217        string = self._script.utilities.substring(entry, 0, -1)
218        msg = "%s %s" % (label, string)
219        voice = self._script.speechGenerator.voice(string=msg)
220        self._script.speakMessage(msg, voice=voice)
221        if detailed or _settingsManager.getSetting('spellcheckSpellSuggestion'):
222            self._script.spellCurrentItem(string)
223
224        return True
225
226    def presentSuggestionListItem(self, detailed=False, includeLabel=False):
227        if not self.isActive():
228            return False
229
230        suggestions = self._suggestionsList
231        if not suggestions:
232            return False
233
234        items = self._script.utilities.selectedChildren(suggestions)
235        if not len(items) == 1:
236            return False
237
238        if includeLabel:
239            label = self._script.utilities.displayedLabel(suggestions) or suggestions.name
240        else:
241            label = ""
242        string = items[0].name
243
244        msg = "%s %s" % (label, string)
245        voice = self._script.speechGenerator.voice(string=msg)
246        self._script.speakMessage(msg.strip(), voice=voice)
247        if detailed or _settingsManager.getSetting('spellcheckSpellSuggestion'):
248            self._script.spellCurrentItem(string)
249
250        if _settingsManager.getSetting('enablePositionSpeaking') \
251           and items[0] == orca_state.locusOfFocus:
252            index, total = self._getSuggestionIndexAndPosition(items[0])
253            msg = object_properties.GROUP_INDEX_SPEECH % {"index": index, "total": total}
254            self._script.speakMessage(msg)
255
256        return True
257
258    def _clearState(self):
259        self._window = None
260        self._errorWidget = None
261        self._changeToEntry = None
262        self._suggestionsList = None
263        self._activated = False
264
265    def _isCandidateWindow(self, window):
266        return False
267
268    def _findChangeToEntry(self, root):
269        return None
270
271    def _findErrorWidget(self, root):
272        return None
273
274    def _findSuggestionsList(self, root):
275        return None
276
277    def _getSuggestionIndexAndPosition(self, suggestion):
278        return -1, -1
279
280    def getAppPreferencesGUI(self):
281
282        from gi.repository import Gtk
283
284        frame = Gtk.Frame()
285        label = Gtk.Label(label="<b>%s</b>" % guilabels.SPELL_CHECK)
286        label.set_use_markup(True)
287        frame.set_label_widget(label)
288
289        alignment = Gtk.Alignment.new(0.5, 0.5, 1, 1)
290        alignment.set_padding(0, 0, 12, 0)
291        frame.add(alignment)
292
293        grid = Gtk.Grid()
294        alignment.add(grid)
295
296        label = guilabels.SPELL_CHECK_SPELL_ERROR
297        value = _settingsManager.getSetting('spellcheckSpellError')
298        self.spellErrorCheckButton = Gtk.CheckButton.new_with_mnemonic(label)
299        self.spellErrorCheckButton.set_active(value)
300        grid.attach(self.spellErrorCheckButton, 0, 0, 1, 1)
301
302        label = guilabels.SPELL_CHECK_SPELL_SUGGESTION
303        value = _settingsManager.getSetting('spellcheckSpellSuggestion')
304        self.spellSuggestionCheckButton = Gtk.CheckButton.new_with_mnemonic(label)
305        self.spellSuggestionCheckButton.set_active(value)
306        grid.attach(self.spellSuggestionCheckButton, 0, 1, 1, 1)
307
308        label = guilabels.SPELL_CHECK_PRESENT_CONTEXT
309        value = _settingsManager.getSetting('spellcheckPresentContext')
310        self.presentContextCheckButton = Gtk.CheckButton.new_with_mnemonic(label)
311        self.presentContextCheckButton.set_active(value)
312        grid.attach(self.presentContextCheckButton, 0, 2, 1, 1)
313
314        return frame
315
316    def getPreferencesFromGUI(self):
317        """Returns a dictionary with the app-specific preferences."""
318
319        return {
320            'spellcheckSpellError': self.spellErrorCheckButton.get_active(),
321            'spellcheckSpellSuggestion': self.spellSuggestionCheckButton.get_active(),
322            'spellcheckPresentContext': self.presentContextCheckButton.get_active()
323        }
324