Package proser :: Module proser
[hide private]
[frames] | no frames]

Source Code for Module proser.proser

  1  #!/usr/bin/env python 
  2   
  3  import sys; 
  4  import os; 
  5  import signal; 
  6  from functools import partial 
  7   
  8  from word_completion.word_collection import WordCollection; 
  9  import python_qt_binding; 
 10  from python_qt_binding.QtGui import QApplication, QMainWindow, QDialog, QPushButton, QTextEdit, QTextCursor, QShortcut, QErrorMessage; 
 11  from python_qt_binding.QtGui import QMessageBox, QWidget; 
 12   
 13  # ----------------------------------------------- Class DialogService ------------------------------------ 
 14   
15 -class DialogService(QWidget):
16 ''' 17 Provides popup windows for information and error messages 18 ''' 19 20 #---------------------------------- 21 # Initializer 22 #-------------- 23
24 - def __init__(self, parent=None):
25 super(DialogService, self).__init__(parent); 26 27 # All-purpose error popup message: 28 # Used by self.showErrorMsgByErrorCode(<errorCode>), 29 # or self.showErrorMsg(<string>). Returns a 30 # QErrorMessage without parent, but with QWindowFlags set 31 # properly to be a dialog popup box: 32 self.errorMsgPopup = QErrorMessage.qtHandler(); 33 # Re-parent the popup, retaining the window flags set 34 # by the qtHandler: 35 self.errorMsgPopup.setParent(parent, self.errorMsgPopup.windowFlags()); 36 #self.errorMsgPopup.setStyleSheet(SpeakEasyGUI.stylesheetAppBG); 37 self.infoMsg = QMessageBox(parent=parent);
38 #self.infoMsg.setStyleSheet(SpeakEasyGUI.stylesheetAppBG); 39 40 #---------------------------------- 41 # showErrorMsg 42 #-------------- 43 QErrorMessage
44 - def showErrorMsg(self,errMsg):
45 ''' 46 Given a string, pop up an error dialog on top of the application window. 47 @param errMsg: The message 48 @type errMsg: string 49 ''' 50 self.errorMsgPopup.showMessage(errMsg);
51 52 #---------------------------------- 53 # showInfoMsg 54 #-------------- 55
56 - def showInfoMsg(self, text):
57 ''' 58 Display a message window with an OK button on top of the application window. 59 @param text: text to display 60 @type text: string 61 ''' 62 self.infoMsg.setText(text); 63 self.infoMsg.exec_();
64 65 #-------------------------------- Proser Class --------------------------- 66
67 -class Proser(QMainWindow):
68 ''' 69 Creates a top level X window in which user can type. Proser provides 70 statistical word completion suggestions. The suggestions are displayed 71 on five buttons at the top of the window. Two methods are available to 72 select one of the suggestions: Click the respective onscreen button, or 73 type one of F5-F9. Completion is based on the word_completion package, 74 which uses a frequency-ranked 6000 word dictionary. 75 76 A Copy button copies the entire typed text into the X cut buffer (a.k.a. clipboard), 77 from where it may be pasted into any other window. 78 79 A special link exists between Proser and SpeakEasy. Proser provides two 80 SpeakEasy related buttons: 81 1. Erase the SpeakEasy text display 82 2. Send the entire Proser window text to SpeakEasy and have it 83 spoken by the currently selected voice. The SpeakEasy window 84 may be minimized during this operation. 85 86 Users may type their text from a physical, or any onscreen keyboard. 87 ''' 88 89 VERSION = "1.0"; 90 SPEAKEASY_PID_PUBLICATION_FILE = "/tmp/speakeasyPID"; 91 NO_COMPLETION_TEXT = ''; 92 FIRST_SHORTCUT_FUNC_KEY = 5; 93 94 # Unix signals for use with clearing text remotely, and with 95 # pasting and speech-triggering from remote: 96 REMOTE_CLEAR_TEXT_SIG = signal.SIGUSR1; 97 REMOTE_PASTE_AND_SPEAK_SIG = signal.SIGUSR2; 98 99
100 - def __init__(self, dictDir=None, userDictFilePath=None):
101 102 super(Proser,self).__init__(); 103 104 # Get the word completion machinery: 105 self.completer = WordCollection(dictDir=dictDir, userDictFilePath=userDictFilePath); 106 107 # Fill our space with the UI: 108 guiPath = os.path.join(os.path.dirname(__file__), 'qt_files/Proser/proser.ui'); 109 self.ui = python_qt_binding.loadUi(guiPath, self); 110 self.setWindowTitle("Proser (V" + Proser.VERSION + ")"); 111 self.completionButtons = [self.wordOption1Button, 112 self.wordOption2Button, 113 self.wordOption3Button, 114 self.wordOption4Button, 115 self.wordOption5Button]; 116 self.clearCompletionButtons(); 117 self.dialogService = DialogService(parent=self); 118 self.connectWidgets(); 119 120 self.show(); 121 self.focusOnTextArea();
122
123 - def connectWidgets(self):
124 ''' 125 Attach slots and widgets to actions. 126 ''' 127 self.clearButton.clicked.connect(self.actionClear); 128 self.copyButton.clicked.connect(self.actionCopy); 129 self.textArea.textChanged.connect(self.actionTextChanged); 130 for buttonObj in self.completionButtons: 131 buttonObj.clicked.connect(partial(self.actionCompletionButton,buttonObj)); 132 133 # Make F5-F9 work as shortcuts for pressing the word suggestion buttons: 134 135 for i in range(len(self.completionButtons) + 1): 136 shortcut = QShortcut(self.tr('F' + str(i + Proser.FIRST_SHORTCUT_FUNC_KEY)), self); 137 # Pass the completion button index (0 through 4) to the handler: 138 shortcut.activated.connect(partial(self.actionKeyShortcut, i)); 139 140 self.addToDictButton.clicked.connect(self.actionAddToDictButton); 141 self.clearSpeakEasyButton.clicked.connect(self.actionClearSpeakEasyText); 142 self.sayButton.clicked.connect(self.actionSendTextToSpeakEasy);
143
144 - def actionClear(self):
145 ''' 146 Clear the text display. 147 ''' 148 self.textArea.clear(); 149 self.clearCompletionButtons(); 150 self.focusOnTextArea();
151
152 - def actionCopy(self):
153 ''' 154 Copy all Proser text to the X cut buffer (i.e. clipboard). 155 ''' 156 currCursor = self.textArea.textCursor(); 157 currCursor.select(QTextCursor.Document); 158 self.textArea.setTextCursor(currCursor); 159 self.textArea.copy(); 160 currCursor = self.textArea.textCursor(); 161 currCursor.clearSelection(); 162 self.textArea.setTextCursor(currCursor); 163 self.focusOnTextArea();
164
165 - def actionTextChanged(self):
166 ''' 167 Act on notification that text in the text panel changed. 168 This notification occurs with every one of the user's keystroke. 169 In response this method updates the text completion buttons. 170 ''' 171 wordSoFar = self.getWordSoFar(); 172 if len(wordSoFar) == 0: 173 self.clearCompletionButtons(); 174 return; 175 completions = self.completer.prefix_search(wordSoFar, cutoffRank=len(self.completionButtons)); 176 if len(completions) == 0: 177 self.clearCompletionButtons(); 178 #print str(completions) 179 self.clearCompletionButtons(); 180 for index,button in enumerate(self.completionButtons): 181 if index >= len(completions): 182 return; 183 button.setText(completions[index]);
184
185 - def actionCompletionButton(self, buttonObj):
186 ''' 187 One of the text completion buttons was pushed. Insert the 188 respective text at the current cursor position. 189 @param buttonObj: the QPushButton object that was pushed. 190 @type buttonObj: QPushButton 191 ''' 192 text = buttonObj.text().encode('UTF-8'); 193 alreadyTypedTxt = self.getWordSoFar(); 194 if len(alreadyTypedTxt) >= len(text): 195 return; 196 textToAppend = text[len(alreadyTypedTxt):] + " "; 197 self.textArea.textCursor().insertText(textToAppend); 198 # Ensure that text area gets focus again: 199 self.focusOnTextArea();
200
201 - def actionKeyShortcut(self, buttonIndex):
202 ''' 203 User pressed a function key F5-F9. Invoke the actionCompletionButton() method. 204 @param buttonIndex: Index 0-4 into the array self.completionButtons. 205 @type buttonIndex: int 206 ''' 207 208 #print 'Function key F' + str(buttonIndex + Proser.FIRST_SHORTCUT_FUNC_KEY) + ' pressed.' 209 try: 210 self.actionCompletionButton(self.completionButtons[buttonIndex]); 211 except IndexError: 212 # don't recognize this function key: 213 pass;
214
215 - def actionAddToDictButton(self):
216 ''' 217 Add selected text to the dictionary that is used for word completion. 218 Words added by this method are appended to the dict_files/dictUserRankAndWord.txt 219 file in the word_completion package. A default rank of 100 is attached. 220 221 The method attempts to warn the user if the text selection seems to span 222 multiple words. In that case, a warning is displayed. A confirmation 223 dialog is raised in case of success. 224 @raise ValueError: if provided rank < 0. 225 ''' 226 # The following used to be a keyword arg, but keyword args don't 227 # seem to work when method is called from PyQt as a button handler. 228 # So the default rank of the new word is set up here now: 229 rank = 100; 230 if rank < 0: 231 raise ValueError("Rank must be greater than or equal to zero"); 232 try: 233 currCursor = self.textArea.textCursor(); 234 selText = currCursor.selectedText().encode('UTF-8'); 235 if len(selText) == 0: 236 self.dialogService.showErrorMsg("Please select a word to be added to the dictionary."); 237 return; 238 if len(selText.split(' ')) != 1 or\ 239 len(selText.split(',')) != 1 or\ 240 len(selText.split('.')) != 1 or\ 241 len(selText.split(';')) != 1 or\ 242 len(selText.split(':')) != 1: 243 self.dialogService.showErrorMsg("Please select only one word to be added to the dictionary."); 244 return; 245 self.completer.addToUserDict(selText, rankInt=rank); 246 self.dialogService.showInfoMsg("Added %s to dictionary." % selText); 247 finally: 248 self.focusOnTextArea();
249 250
251 - def getWordSoFar(self):
252 ''' 253 Service method to retrieve the most recent partially typed word. 254 ''' 255 currCursor = self.textArea.textCursor(); 256 currCursor.select(QTextCursor.WordUnderCursor); 257 wordFragment = currCursor.selectedText().encode('UTF-8'); 258 #print "Frag (cur at: " + str(currCursor.position()) + "): " + str(wordFragment); 259 return wordFragment;
260
261 - def clearCompletionButtons(self):
262 ''' 263 Service method to clear labels on all word completion buttons. 264 ''' 265 for completionButton in self.completionButtons: 266 completionButton.setText(completionButton.setText(Proser.NO_COMPLETION_TEXT));
267
268 - def focusOnTextArea(self):
269 ''' 270 Service method to force the cursor focus into the text area. 271 ''' 272 self.textArea.setFocus();
273
275 ''' 276 Copy current content of the text field into the X clipboard. 277 Cause a running SpeakEasy process to paste that newly loaded X clipboard into 278 its SpeakEasy text area, and to speak the content using the current voice. 279 The method getSpeakEasyPID() is called from here, and that method will 280 raise a warning dialog if no SpeakEasy process is currently running. 281 That method will also raise the ValueError documented below. 282 283 Implementation: Send a Unix signal REMOTE_PASTE_AND_SPEAK_SIG to the 284 SpeakEasy application, if one is running. 285 286 @raise ValueError: if the file /tmp/speakeasyPID does not contain an integer. That 287 file is initialized by SpeakEasy with that process' PID. While that 288 pid might be stale, it would still be an integer, unless the file 289 is changed manually. 290 ''' 291 pid = self.getSpeakEasyPID(); 292 if pid is None: 293 # Error message was already provided by getSpeakEasyPID 294 return; 295 self.actionCopy(); 296 try: 297 os.kill(pid, Proser.REMOTE_PASTE_AND_SPEAK_SIG); 298 except OSError: 299 # PID was invalid: 300 self.dialogService.showErrorMsg("SpeakEasy application seems not to be running. Please start it.");
301
302 - def actionClearSpeakEasyText(self):
303 ''' 304 Cause a running SpeakEasy process to clear its text area. 305 306 Implementation: Send a Unix signal REMOTE_CLEAR_TEXT_SIG to 307 the SpeakEasy process if one is running. Else a warning dialog is raised. 308 309 @raise ValueError: if the file /tmp/speakeasyPID does not contain an integer. That 310 file is initialized by SpeakEasy with that process' PID. While that 311 pid might be stale, it would still be an integer, unless the file 312 is changed manually. 313 ''' 314 pid = self.getSpeakEasyPID(); 315 if pid is None: 316 # Error message was already provided by getSpeakEasyPID 317 return; 318 try: 319 os.kill(pid, Proser.REMOTE_CLEAR_TEXT_SIG); 320 except OSError: 321 # PID was invalid: 322 self.dialogService.showErrorMsg("SpeakEasy application seems not to be running. Please start it.");
323
324 - def getSpeakEasyPID(self):
325 ''' 326 Return the PID of the SpeakEasy application, if it is running. 327 Else return None. The PID is communicated via a file. Note that 328 this file's content might be stale. So callers must protect against 329 the target process not running any more. 330 331 @raise ValueError: if the file /tmp/speakeasyPID does not contain an integer. That 332 file is initialized by SpeakEasy with that process' PID. While that 333 pid might be stale, it would still be an integer, unless the file 334 is changed manually. 335 ''' 336 try: 337 pidFile = os.fdopen(os.open(Proser.SPEAKEASY_PID_PUBLICATION_FILE, os.O_RDONLY)); 338 except OSError: 339 self.dialogService.showErrorMsg("SpeakEasy application seems not to be running. Please start it."); 340 return None; 341 try: 342 pid = int(pidFile.readline()); 343 except ValueError: 344 # PID file did not contain an integer: 345 self.dialogService.showErrorMsg("SpeakEasy PID file did not contain an integer. Please notify the developer."); 346 return None; 347 348 return pid;
349 350 351 if __name__ == '__main__': 352 353 app = QApplication(sys.argv); 354 #QApplication.setStyle(QCleanlooksStyle()) 355 proser = Proser(); 356 app.exec_(); 357 sys.exit(); 358