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