Module morseInput_ui
[hide private]
[frames] | no frames]

Source Code for Module morseInput_ui

   1  #!/usr/bin/env python 
   2   
   3  # To do: 
   4  # - Volume control 
   5  # - Somewhere (moveEvent()?_: ensure that no overlap of morse win with active window. 
   6  #      If so, show error msg.  (Just put into Doc 
   7  # - Occasional X error: output in startup window, and cursor runs into dot/dash buttons. 
   8  # - Publish package 
   9   
  10  # Doc: 
  11  #   - Abort if mouse click in rest zone. 
  12  #   - Left click to suspend beeping and cursor contraint and timing measure 
  13  #   - Right click: recenter cursor 
  14  #   - Prefs in $HOME/.morser/morser.cfg 
  15  #   - Cheat sheet (Menu) 
  16  #   - Options window 
  17  #   - Crosshair blinks yellow when word separation detected. 
  18  #   - If output gibberish, and you know your Morse was good, *lower* inter letter dwell time 
  19  # 
  20  # Needed PYTHONPATH: 
  21  #   /opt/ros/fuerte/lib/python2.7/dist-packages:/home/paepcke/fuerte/stacks/robhum_ui_utils:/home/paepcke/fuerte/stacks/robhum_ui_utils/gesture_buttons/src:/opt/ros/fuerte/stacks/python_qt_binding/src:/home/paepcke/fuerte/stacks/robhum_ui_utils/qt_comm_channel/src:/home/paepcke/fuerte/stacks/robhum_ui_utils/qt_dialog_service/src:/home/paepcke/fuerte/stacks/robhum_ui_utils/virtual_keyboard/src: 
  22   
  23  # Dash/Dot blue value: 35,60,149 
  24   
  25  import roslib; roslib.load_manifest('morseInput') 
  26   
  27  import sys 
  28  import os 
  29  import re 
  30  import fcntl 
  31  import ConfigParser 
  32  from functools import partial 
  33   
  34  from gesture_buttons.gesture_button import GestureButton 
  35  from gesture_buttons.gesture_button import FlickDirection 
  36   
  37  from qt_comm_channel.commChannel import CommChannel 
  38  from qt_dialog_service.qt_dialog_service import DialogService 
  39   
  40  from morseToneGeneration import MorseGenerator 
  41  from morseToneGeneration import Morse 
  42  from morseToneGeneration import TimeoutReason 
  43   
  44  from morseCheatSheet import MorseCheatSheet; 
  45   
  46  from morseSpeedTimer import MorseSpeedTimer; 
  47   
  48  from virtual_keyboard.virtual_keyboard import VirtualKeyboard 
  49   
  50  from python_qt_binding import loadUi; 
  51  from python_qt_binding import QtGui; 
  52  from python_qt_binding import QtCore; 
  53  #from word_completion.word_collection import WordCollection; 
  54  from QtGui import QApplication, QMainWindow, QMessageBox, QWidget, QCursor, QHoverEvent, QColor, QIcon; 
  55  from QtGui import QMenuBar, QToolTip, QLabel, QPixmap, QRegExpValidator; 
  56  from QtCore import QPoint, Qt, QTimer, QEvent, Signal, QCoreApplication, QRect, QRegExp;  
57 58 # Dot/Dash RGB: 0,179,240 59 60 -class OutputType:
61 TYPE = 0 62 SPEAK = 1
63
64 -class Direction:
65 HORIZONTAL = 0 66 VERTICAL = 1
67
68 -class Crosshairs:
69 CLEAR = 0 70 YELLOW = 1 71 GREEN = 2 72 RED = 3
73
74 -class PanelExpansion:
75 LESS = 0 76 MORE = 1
77
78 -class MorseInputSignals(CommChannel):
79 letterDone = Signal(int,str); 80 panelCollapsed = Signal(int,int,int,int);
81
82 -class MorseInput(QMainWindow):
83 ''' 84 Manages all UI interactions with the Morse code generation. 85 ''' 86 87 MORSE_BUTTON_WIDTH = 100; #px 88 MORSE_BUTTON_HEIGHT = 100; #px 89 90 SUPPORT_BUTTON_WIDTHS = 80; #px: The maximum Space and Backspace button widths. 91 SUPPORT_BUTTON_HEIGHTS = 80; #px: The maximum Space and Backspace button heights. 92 93 MOUSE_UNCONSTRAIN_TIMEOUT = 300; # msec 94 95 HEAD_TRACKER = True; 96
97 - def __init__(self):
98 super(MorseInput,self).__init__(); 99 100 # Only allow a single instance of the Morser program to run: 101 self.morserLockFile = '/tmp/morserLock.lk'; 102 MorseInput.morserLockFD = open(self.morserLockFile, 'w') 103 try: 104 # Attempt to lock the lock file exclusively, but 105 # throw IOError if already locked, rather than waiting 106 # for unlock (the LOCK_NB ORing): 107 fcntl.lockf(MorseInput.morserLockFD, fcntl.LOCK_EX | fcntl.LOCK_NB) 108 except IOError: 109 errMsg = "The Morser program is already running. Please quit it.\n" +\ 110 "If Morser really is not running, execute 'rm %s' in a terminal window." % self.morserLockFile; 111 sys.stderr.write(errMsg) 112 sys.exit() 113 114 # Disallow focus acquisition for the morse window. 115 # Needed to prevent preserve focus on window that 116 # is supposed to receive the clear text of the 117 # morse: 118 self.setFocusPolicy(Qt.NoFocus); 119 120 self.iconDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'icons') 121 122 CommChannel.registerSignals(MorseInputSignals); 123 124 # Find QtCreator's XML file in the PYTHONPATH, and load it: 125 currDir = os.path.realpath(__file__); 126 127 # Load UI for Morse input: 128 relPathQtCreatorFileMainWin = "qt_files/morseInput/morseInput.ui"; 129 qtCreatorXMLFilePath = self.findFile(relPathQtCreatorFileMainWin); 130 if qtCreatorXMLFilePath is None: 131 raise ValueError("Can't find QtCreator user interface file %s" % relPathQtCreatorFileMainWin); 132 # Make QtCreator generated UI a child if this instance: 133 loadUi(qtCreatorXMLFilePath, self); 134 self.windowTitle = "Morser: Semi-automatic Morse code input"; 135 self.setWindowTitle(self.windowTitle); 136 137 # Load UI for Morse options dialog: 138 relPathQtCreatorFileOptionsDialog = "qt_files/morserOptions/morseroptions.ui"; 139 qtCreatorXMLFilePath = self.findFile(relPathQtCreatorFileOptionsDialog); 140 if qtCreatorXMLFilePath is None: 141 raise ValueError("Can't find QtCreator user interface file %s" % relPathQtCreatorFileOptionsDialog); 142 # Make QtCreator generated UI a child if this instance: 143 self.morserOptionsDialog = loadUi(qtCreatorXMLFilePath); 144 145 # Load UI for Morse Cheat Sheet: 146 self.morseCheatSheet = MorseCheatSheet(self); 147 148 self.dialogService = DialogService(); 149 150 # Get a morse generator that manages all Morse 151 # generation and timing: 152 self.morseGenerator = MorseGenerator(callback=MorseInput.letterCompleteNotification); 153 154 # Get virtual keyboard that can 'fake' X11 keyboard inputs: 155 self.virtKeyboard = VirtualKeyboard(); 156 157 # setOptions() needs to be called after instantiation 158 # of morseGenerator, so that we can obtain the generator's 159 # defaults for timings: 160 self.optionsFilePath = os.path.join(os.getenv('HOME'), '.morser/morser.cfg'); 161 self.setOptions(); 162 163 # Create the gesture buttons for dot/dash/space/backspace: 164 self.insertGestureButtons(); 165 GestureButton.setFlicksEnabled(False); 166 167 self.installMenuBar(); 168 self.installStatusBar(); 169 170 # Get a speed measurer (must be defined before 171 # call to connectWidgets(): 172 self.speedMeasurer = MorseSpeedTimer(self); 173 174 self.connectWidgets(); 175 self.cursorEnteredOnce = False; 176 177 # Set cursor to hand icon while inside Morser: 178 self.morseCursor = QCursor(Qt.OpenHandCursor); 179 QApplication.setOverrideCursor(self.morseCursor); 180 #QApplication.restoreOverrideCursor() 181 182 self.blinkTimer = None; 183 self.flashTimer = None; 184 185 # Init capability of constraining cursor to 186 # move only virtically and horizontally: 187 self.initCursorConstrainer(); 188 189 # Don't allow editing of the ticker tape: 190 self.tickerTapeLineEdit.setFocusPolicy(Qt.NoFocus); 191 # But allow anything to be placed inside programmatically: 192 tickerTapeRegExp = QRegExp('.*'); 193 tickerTapeValidator = QRegExpValidator(tickerTapeRegExp); 194 self.tickerTapeLineEdit.setValidator(tickerTapeValidator); 195 196 # Deceleration readout is floating point with up to 2 digits after decimal: 197 cursorDecelerationRegExp = QRegExp(r'[\d]{1}[.]{1}[\d]{1,2}$'); 198 cursorDecelerationValidator = QRegExpValidator(cursorDecelerationRegExp); 199 self.morserOptionsDialog.cursorDecelerationReadoutLineEdit.setValidator(cursorDecelerationValidator); 200 201 self.expandPushButton.setFocusPolicy(Qt.NoFocus); 202 203 # Styling: 204 self.createColors(); 205 self.setStyleSheet("QWidget{background-color: %s}" % self.lightBlueColor.name()); 206 207 # Power state: initially on: 208 self.poweredUp = True; 209 210 self.show(); 211 212 # Compute global x positions of dash/dot buttons facing 213 # towards the rest area: 214 self.computeInnerButtonEdges(); 215 216 # Monitor mouse, so that we can constrain mouse movement to 217 # vertical and horizontal (must be set after the affected 218 # widget(s) are visible): 219 #self.setMouseTracking(True) 220 self.centralWidget.installEventFilter(self); 221 self.centralWidget.setMouseTracking(True)
222
223 - def initCursorConstrainer(self):
224 self.recentMousePos = None; 225 self.currentMouseDirection = None; 226 self.enableConstrainVertical = False; 227 # Holding left mouse button inside the Morse 228 # window will suspend cursor constraining, 229 # if it is enabled. Letting go of the button 230 # will re-enable constraints. This var 231 # keeps track of suspension so mouse-button-up 232 # knows whether to re-instate constraining: 233 self.cursorContraintSuspended = False; 234 235 # Timer that frees the cursor from 236 # vertical/horizontal constraint every few 237 # milliseconds, unless mouse keeps moving: 238 self.mouseUnconstrainTimer = QTimer(); 239 self.mouseUnconstrainTimer.setInterval(MorseInput.MOUSE_UNCONSTRAIN_TIMEOUT); 240 self.mouseUnconstrainTimer.setSingleShot(True); 241 self.mouseUnconstrainTimer.timeout.connect(self.unconstrainTheCursor);
242
243 - def computeInnerButtonEdges(self):
244 # Remember the X position of the global-screen right 245 # edge of the dot button for reference in the event filter: 246 localGeo = self.dotButton.geometry(); 247 dotButtonGlobalPos = self.mapToGlobal(QPoint(localGeo.x() + localGeo.width(), 248 localGeo.y())); 249 self.dotButtonGlobalRight = dotButtonGlobalPos.x(); 250 # Remember the X position of the global-screen left 251 # edge of the dash button for reference in the event filter: 252 localGeo = self.dashButton.geometry(); 253 dashButtonGlobalPos = self.mapToGlobal(QPoint(localGeo.x(), localGeo.y())); 254 self.dashButtonGlobalLeft = dashButtonGlobalPos.x(); 255 256 # Remember global location of the central point in the rest zone: 257 self.crosshairLabel.setMaximumHeight(11); # Height of crosshair icon 258 self.crosshairLabel.setMaximumWidth(11); # Width of crosshair icon 259 # Compute the location of the crosshair in the center of 260 # the rest area. The addition of 20 pixels to the Y-coordinate 261 # accounts for Ubuntu's title bar at the top of the display, 262 # which mapToGlobal() does not account for: 263 self.centralRestGlobalPos = self.mapToGlobal(self.crosshairLabel.pos() + QPoint(0,20));
264
265 - def startCursorConstraint(self):
266 self.constrainCursorInHotZone = True;
267
268 - def stopCursorConstraint(self):
269 self.constrainCursorInHotZone = False; 270 self.mouseUnconstrainTimer.stop();
271
272 - def createColors(self):
273 self.grayBlueColor = QColor(89,120,137); # Letter buttons 274 self.lightBlueColor = QColor(206,230,243); # Background 275 self.darkGray = QColor(65,88,101); # Central buttons 276 self.wordListFontColor = QColor(62,143,185); # Darkish blue. 277 self.purple = QColor(147,124,195); # Gesture button pressed
278 279
280 - def findFile(self, path, matchFunc=os.path.isfile):
281 if path is None: 282 return None 283 for dirname in sys.path: 284 candidate = os.path.join(dirname, path) 285 if matchFunc(candidate): 286 return candidate 287 return None;
288
289 - def insertGestureButtons(self):
290 291 self.dotButton = GestureButton('dot'); 292 self.dotButton.setIcon(QIcon(os.path.join(self.iconDir, 'dot.png'))); 293 self.dotButton.setText(""); 294 # Don't have button assume the pressed-down color when 295 # clicked: 296 self.dotButton.setFocusPolicy(Qt.NoFocus); 297 self.dotButton.setMinimumHeight(MorseInput.MORSE_BUTTON_HEIGHT); 298 self.dotButton.setMinimumWidth(MorseInput.MORSE_BUTTON_WIDTH); 299 self.dotAndDashHLayout.addWidget(self.dotButton); 300 301 self.dotAndDashHLayout.addStretch(); 302 303 # Crosshair: 304 self.crosshairPixmapClear = QPixmap(os.path.join(self.iconDir, 'crosshairEmpty.png')); 305 self.crosshairPixmapGreen = QPixmap(os.path.join(self.iconDir, 'crosshairGreen.png')); 306 self.crosshairPixmapYellow = QPixmap(os.path.join(self.iconDir, 'crosshairYellow.png')); 307 self.crosshairPixmapRED = QPixmap(os.path.join(self.iconDir, 'crosshairRed.png')); 308 self.crosshairLabel = QLabel(); 309 self.crosshairLabel.setPixmap(self.crosshairPixmapClear); 310 self.crosshairLabel.setText(""); 311 self.dotAndDashHLayout.addWidget(self.crosshairLabel); 312 313 self.dotAndDashHLayout.addStretch(); 314 315 self.dashButton = GestureButton('dash'); 316 self.dashButton.setIcon(QIcon(os.path.join(self.iconDir, 'dash.png'))); 317 self.dashButton.setText(""); 318 # Don't have button assume the pressed-down color when 319 # clicked: 320 self.dashButton.setFocusPolicy(Qt.NoFocus); 321 self.dashButton.setMinimumHeight(MorseInput.MORSE_BUTTON_HEIGHT); 322 self.dashButton.setMinimumWidth(MorseInput.MORSE_BUTTON_WIDTH); 323 self.dotAndDashHLayout.addWidget(self.dashButton); 324 325 self.eowButton = GestureButton('Space'); 326 self.eowButton.setAutoRepeat(True); 327 # Don't have button assume the pressed-down color when 328 # clicked: 329 self.eowButton.setFocusPolicy(Qt.NoFocus); 330 self.eowButton.setMaximumWidth(MorseInput.SUPPORT_BUTTON_WIDTHS) 331 self.eowButton.setMinimumHeight(MorseInput.SUPPORT_BUTTON_HEIGHTS) 332 self.endOfWordButtonHLayout.addWidget(self.eowButton); 333 334 self.backspaceButton = GestureButton('Backspace'); 335 self.backspaceButton.setAutoRepeat(True); 336 # Don't have button assume the pressed-down color when 337 # clicked: 338 self.backspaceButton.setFocusPolicy(Qt.NoFocus); 339 self.backspaceButton.setMaximumWidth(MorseInput.SUPPORT_BUTTON_WIDTHS) 340 self.backspaceButton.setMinimumHeight(MorseInput.SUPPORT_BUTTON_HEIGHTS) 341 self.backspaceHLayout.addWidget(self.backspaceButton); 342 343 self.powerPushButton.setIcon(QIcon(os.path.join(self.iconDir, 'powerIconSmall.png'))); 344 # Don't have button assume the pressed-down color when 345 # clicked: 346 self.powerPushButton.setFocusPolicy(Qt.NoFocus); 347 self.powerPushButton.setChecked(True); 348 349 # Prevent focus on the two clear buttons: 350 self.speedMeasureClearButton.setFocusPolicy(Qt.NoFocus); 351 self.tickerTapeClearButton.setFocusPolicy(Qt.NoFocus); 352 self.timeMeButton.setFocusPolicy(Qt.NoFocus);
353
354 - def installMenuBar(self):
355 exitAction = QtGui.QAction(QtGui.QIcon('exit.png'), '&Exit', self) 356 exitAction.setShortcut('Ctrl+Q') 357 exitAction.setStatusTip('Exit application') 358 exitAction.triggered.connect(self.close) 359 360 raiseOptionsDialogAction = QtGui.QAction(QtGui.QIcon('preferences-desktop-accessibility.png'), '&Options', self) 361 362 raiseOptionsDialogAction.setShortcut('Ctrl+O'); 363 raiseOptionsDialogAction.setStatusTip('Show options and settings') 364 raiseOptionsDialogAction.triggered.connect(self.showOptions) 365 366 raiseCheatSheetAction = QtGui.QAction(QtGui.QIcon('preferences-desktop-accessibility.png'), '&Cheat sheet', self) 367 raiseCheatSheetAction.setShortcut('Ctrl+M') 368 raiseCheatSheetAction.setStatusTip('Show Morse code cheat sheet') 369 raiseCheatSheetAction.triggered.connect(self.showCheatSheet) 370 371 fileMenu = self.menuBar.addMenu('&File') 372 fileMenu.addAction(exitAction) 373 374 editMenu = self.menuBar.addMenu('&Edit') 375 editMenu.addAction(raiseOptionsDialogAction) 376 377 viewMenu = self.menuBar.addMenu('&View') 378 viewMenu.addAction(raiseCheatSheetAction) 379 # When showing table the first time, we move it left so that it 380 # does not totally obscure the Morse window: 381 self.shownCheatSheetBefore = False;
382
383 - def installStatusBar(self):
384 self.statusBar.showMessage("Ready to go... Remember to set focus");
385 386
387 - def connectWidgets(self):
388 389 # Signal connections: 390 CommChannel.getSignal('GestureSignals.buttonEnteredSig').connect(self.buttonEntered); 391 CommChannel.getSignal('GestureSignals.buttonExitedSig').connect(self.buttonExited); 392 CommChannel.getSignal('MorseInputSignals.letterDone').connect(self.deliverInput); 393 CommChannel.getSignal('MorseInputSignals.panelCollapsed').connect(self.adjustMainWindowHeight); 394 395 # Main window: 396 self.timeMeButton.pressed.connect(self.speedMeasurer.timeMeToggled); 397 self.tickerTapeClearButton.clicked.connect(self.tickerTapeClear); 398 self.powerPushButton.pressed.connect(self.powerToggled); 399 self.expandPushButton.clicked.connect(self.togglePanelExpansion); 400 401 # Options dialog: 402 self.morserOptionsDialog.cursorConstraintCheckBox.stateChanged.connect(partial(self.checkboxStateChanged, 403 self.morserOptionsDialog.cursorConstraintCheckBox)); 404 self.morserOptionsDialog.wordStopSegmentationCheckBox.stateChanged.connect(partial(self.checkboxStateChanged, 405 self.morserOptionsDialog.wordStopSegmentationCheckBox)); 406 self.morserOptionsDialog.typeOutputRadioButton.toggled.connect(partial(self.checkboxStateChanged, 407 self.morserOptionsDialog.typeOutputRadioButton)); 408 self.morserOptionsDialog.speechOutputRadioButton.toggled.connect(partial(self.checkboxStateChanged, 409 self.morserOptionsDialog.speechOutputRadioButton)); 410 self.morserOptionsDialog.useTickerCheckBox.toggled.connect(partial(self.checkboxStateChanged, 411 self.morserOptionsDialog.useTickerCheckBox)); 412 413 self.morserOptionsDialog.cursorDecelerationSlider.valueChanged.connect(partial(self.sliderStateChanged, 414 self.morserOptionsDialog.cursorDecelerationSlider)); 415 self.morserOptionsDialog.keySpeedSlider.valueChanged.connect(partial(self.sliderStateChanged, 416 self.morserOptionsDialog.keySpeedSlider)); 417 self.morserOptionsDialog.interLetterDelaySlider.valueChanged.connect(partial(self.sliderStateChanged, 418 self.morserOptionsDialog.interLetterDelaySlider)); 419 self.morserOptionsDialog.interWordDelaySlider.valueChanged.connect(partial(self.sliderStateChanged, 420 self.morserOptionsDialog.interWordDelaySlider)); 421 422 self.morserOptionsDialog.savePushButton.clicked.connect(self.optionsSaveButton); 423 self.morserOptionsDialog.cancelPushButton.clicked.connect(self.optionsCancelButton); 424 425 self.morserOptionsDialog.cursorDecelerationReadoutLineEdit.editingFinished.connect(partial(self.sliderReadoutModified, 426 self.morserOptionsDialog.cursorDecelerationSlider)); 427 self.morserOptionsDialog.cursorDecelerationReadoutLineEdit.returnPressed.connect(partial(self.sliderReadoutModified, 428 self.morserOptionsDialog.cursorDecelerationSlider)); 429 self.morserOptionsDialog.keySpeedReadoutLineEdit.editingFinished.connect(partial(self.sliderReadoutModified, 430 self.morserOptionsDialog.keySpeedSlider)); 431 self.morserOptionsDialog.keySpeedReadoutLineEdit.returnPressed.connect(partial(self.sliderReadoutModified, 432 self.morserOptionsDialog.keySpeedSlider)); 433 self.morserOptionsDialog.letterDwellReadoutLineEdit.editingFinished.connect(partial(self.sliderReadoutModified, 434 self.morserOptionsDialog.interLetterDelaySlider)); 435 self.morserOptionsDialog.letterDwellReadoutLineEdit.returnPressed.connect(partial(self.sliderReadoutModified, 436 self.morserOptionsDialog.interLetterDelaySlider)); 437 self.morserOptionsDialog.wordDwellReadoutLineEdit.editingFinished.connect(partial(self.sliderReadoutModified, 438 self.morserOptionsDialog.interWordDelaySlider)); 439 self.morserOptionsDialog.wordDwellReadoutLineEdit.returnPressed.connect(partial(self.sliderReadoutModified, 440 self.morserOptionsDialog.interWordDelaySlider));
441
442 - def expandMorePanel(self, expansion):
443 ''' 444 Expand or hide the main window's bottom panel, which holds the speed meter. 445 @param expansion: expand vs. hide 446 @type expansion: PanelExpansion 447 ''' 448 if expansion == PanelExpansion.LESS: 449 self.expandPushButton.setIcon(QIcon(os.path.join(self.iconDir, 'plusSign.png'))); 450 self.speedMeasureWidget.setHidden(True); 451 # Remember this state in configuration: 452 self.cfgParser.set('Appearance','morePanelExpanded',str(False)); 453 newMainWinGeo = self.getAdjustedWinGeo(-1 * self.speedMeasureWidget.geometry().height()); 454 MorseInputSignals.getSignal('MorseInputSignals.panelCollapsed').emit(newMainWinGeo.x(), 455 newMainWinGeo.y(), 456 newMainWinGeo.width(), 457 newMainWinGeo.height()); 458 else: 459 self.expandPushButton.setIcon(QIcon(os.path.join(self.iconDir, 'minusSign.png'))); 460 self.speedMeasureWidget.setHidden(False); 461 self.cfgParser.set('Appearance','morePanelExpanded',str(True));
462
463 - def getAdjustedWinGeo(self, pixels):
464 ''' 465 Given a positive or negative number of pixels, 466 return a rectangle that is respectively higher or 467 shorter than the current main window. 468 @param pixels: number of pixels to add or subtract from the height 469 @type pixels: int 470 @return: new rectangle 471 @rtype: QRect 472 ''' 473 geo = self.geometry(); 474 newHeight = geo.height() + pixels; 475 newGeo = QRect(geo.x(), geo.y(), geo.width(), newHeight); 476 return newGeo;
477 478 @QtCore.Slot(int,int,int,int)
479 - def adjustMainWindowHeight(self, x,y,width,height):
480 ''' 481 Given a rectangle, adjust the main window dimensions 482 to take that shape. 483 @param x: 484 @type x: int 485 @param y: 486 @type y: int 487 @param width: 488 @type width: int 489 @param height: 490 @type height: int 491 ''' 492 self.setMaximumHeight(self.geometry().height() - self.speedMeasureWidget.geometry().height());
493
494 - def togglePanelExpansion(self):
495 if self.speedMeasureWidget.isVisible(): 496 self.expandMorePanel(PanelExpansion.LESS); 497 else: 498 self.expandMorePanel(PanelExpansion.MORE);
499
500 - def showOptions(self):
501 self.morserOptionsDialog.show();
502
503 - def showCheatSheet(self):
504 self.morseCheatSheet.show(); 505 if not self.shownCheatSheetBefore: 506 cheatSheetGeo = self.morseCheatSheet.geometry(); 507 cheatSheetGeo.moveLeft(200); 508 self.morseCheatSheet.setGeometry(cheatSheetGeo); 509 self.shownCheatSheetBefore = True;
510
511 - def setOptions(self):
512 513 self.optionsDefaultDict = { 514 'outputDevice' : str(OutputType.TYPE), 515 'letterDwellSegmentation' : str(True), 516 'wordDwellSegmentation' : str(True), 517 'constrainCursorInHotZone' : str(False), 518 'keySpeed' : str(1.7), 519 'cursorDeceleration' : str(0.5), 520 'interLetterDwellDelay' : str(self.morseGenerator.getInterLetterTime()), 521 'interWordDwellDelay' : str(self.morseGenerator.getInterWordTime()), 522 'winGeometry' : '100,100,350,350', 523 'useTickerTape' : str(True), 524 'morePanelExpanded' : str(True), 525 } 526 527 self.cfgParser = ConfigParser.SafeConfigParser(self.optionsDefaultDict); 528 self.cfgParser.add_section('Morse generation'); 529 self.cfgParser.add_section('Output'); 530 self.cfgParser.add_section('Appearance'); 531 self.cfgParser.read(self.optionsFilePath); 532 533 mainWinGeometry = self.cfgParser.get('Appearance', 'winGeometry'); 534 # Get four ints from the comma-separated string of upperLeftX, upperLeftY, 535 # Width,Height numbers: 536 try: 537 nums = mainWinGeometry.split(','); 538 self.setGeometry(QRect(int(nums[0].strip()),int(nums[1].strip()),int(nums[2].strip()),int(nums[3].strip()))); 539 except Exception as e: 540 self.dialogService.showErrorMsg("Could not set window size; config file spec not grammatical: %s. (%s" % (mainWinGeometry, `e`)); 541 542 self.setCursorDeceleration(self.cfgParser.getfloat('Morse generation', 'cursorDeceleration')); 543 544 self.morseGenerator.setInterLetterDelay(self.cfgParser.getfloat('Morse generation', 'interLetterDwellDelay')); 545 self.morseGenerator.setInterWordDelay(self.cfgParser.getfloat('Morse generation', 'interWordDwellDelay')); 546 self.morseGenerator.setSpeed(self.cfgParser.getfloat('Morse generation', 'keySpeed')); 547 548 self.constrainCursorInHotZone = self.cfgParser.getboolean('Morse generation', 'constrainCursorInHotZone'); 549 self.outputDevice = self.cfgParser.getint('Output', 'outputDevice'); 550 self.letterDwellSegmentation = self.cfgParser.getboolean('Morse generation', 'letterDwellSegmentation'); 551 self.letterDwellSegmentation = self.cfgParser.getboolean('Morse generation', 'wordDwellSegmentation'); 552 553 self.useTickerTape = self.cfgParser.getboolean('Output', 'useTickerTape'); 554 555 self.panelExpanded = self.cfgParser.getboolean('Appearance', 'morePanelExpanded'); 556 if self.panelExpanded: 557 self.expandMorePanel(PanelExpansion.MORE); 558 else: 559 self.expandMorePanel(PanelExpansion.LESS); 560 561 # Make the options dialog reflect the options we just established: 562 # Path to Morser options file: 563 self.initOptionsDialogFromOptions();
564
566 567 # Cursor constraint: 568 self.morserOptionsDialog.cursorConstraintCheckBox.setChecked(self.cfgParser.getboolean('Morse generation', 'constrainCursorInHotZone')); 569 570 # Automatic word segmentation: 571 enableWordSegmentation = self.cfgParser.getboolean('Morse generation', 'wordDwellSegmentation'); 572 self.morserOptionsDialog.wordStopSegmentationCheckBox.setChecked(enableWordSegmentation); 573 if not enableWordSegmentation: 574 self.morserOptionsDialog.interWordDelaySlider.setEnabled(False); 575 self.morserOptionsDialog.wordDwellReadoutLineEdit.setEnabled(False); 576 577 # Output to X11 vs. Speech: 578 self.morserOptionsDialog.typeOutputRadioButton.setChecked(self.cfgParser.getint('Output', 'outputDevice')==OutputType.TYPE); 579 self.morserOptionsDialog.speechOutputRadioButton.setChecked(self.cfgParser.getint('Output', 'outputDevice')==OutputType.SPEAK); 580 581 582 # Cursor deceleration: 583 cursorDeceleration = self.cfgParser.getfloat('Morse generation', 'cursorDeceleration'); 584 self.morserOptionsDialog.cursorDecelerationSlider.setValue(int(cursorDeceleration * 100)); 585 # Readout for the cursor deceleration: 586 self.morserOptionsDialog.cursorDecelerationReadoutLineEdit.setText(str(cursorDeceleration)); 587 588 # Key speed slider: 589 self.morserOptionsDialog.keySpeedSlider.setValue(int(10*self.cfgParser.getfloat('Morse generation', 'keySpeed'))); 590 # Init the readout of the speed slider: 591 self.morserOptionsDialog.keySpeedReadoutLineEdit.setText(str(self.morserOptionsDialog.keySpeedSlider.value())); 592 593 # Dwell time that indicates end of Morse letter: 594 interLetterSecs = self.cfgParser.getfloat('Morse generation', 'interLetterDwellDelay'); 595 self.morserOptionsDialog.interLetterDelaySlider.setValue(int(interLetterSecs*1000.)); # inter-letter dwell slider is in msecs 596 # Init the readout of the letter dwell slider: 597 self.morserOptionsDialog.letterDwellReadoutLineEdit.setText(str(self.morserOptionsDialog.interLetterDelaySlider.value())); 598 599 # Dwell time that indicates end of word: 600 interWordSecs = self.cfgParser.getfloat('Morse generation', 'interWordDwellDelay'); 601 self.morserOptionsDialog.interWordDelaySlider.setValue(int(interWordSecs*1000.)); # inter-word dwell slider is in msecs 602 # Init the readout of the word dwell slider: 603 self.morserOptionsDialog.wordDwellReadoutLineEdit.setText(str(self.morserOptionsDialog.interWordDelaySlider.value())); 604 self.morserOptionsDialog.useTickerCheckBox.setChecked(self.cfgParser.getboolean('Output', 'useTickerTape'));
605
606 - def sliderReadoutModified(self, slider):
607 if slider == self.morserOptionsDialog.cursorDecelerationSlider: 608 slider.setValue(int(float(self.morserOptionsDialog.cursorDecelerationReadoutLineEdit.text()) * 100.0)); 609 elif slider == self.morserOptionsDialog.keySpeedSlider: 610 slider.setValue(int(self.morserOptionsDialog.keySpeedReadoutLineEdit.text())); 611 elif slider == self.morserOptionsDialog.interLetterDelaySlider: 612 slider.setValue(int(self.morserOptionsDialog.letterDwellReadoutLineEdit.text())); 613 elif slider == self.morserOptionsDialog.interWordDelaySlider: 614 slider.setValue(int(self.morserOptionsDialog.wordDwellReadoutLineEdit.text())); 615 else: 616 raise ValueError("Expecting one of the options slider objects.") 617 slider.setFocus();
618 619
620 - def checkboxStateChanged(self, checkbox, newState):
621 ''' 622 Called when any of the option dialog's checkboxes change: 623 @param checkbox: the affected checkbox 624 @type checkbox: QCheckBox 625 @param newState: the new state, though Qt docs are cagey about what this means: an int of some kind. 626 @type newState: QCheckState 627 ''' 628 checkboxNowChecked = checkbox.isChecked(); 629 if checkbox == self.morserOptionsDialog.cursorConstraintCheckBox: 630 self.cfgParser.set('Morse generation','constrainCursorInHotZone',str(checkboxNowChecked)); 631 self.constrainCursorInHotZone = checkboxNowChecked; 632 elif checkbox == self.morserOptionsDialog.useTickerCheckBox: 633 self.cfgParser.set('Output','useTickerTape', str(checkboxNowChecked)); 634 self.useTickerTape = checkboxNowChecked; 635 elif checkbox == self.morserOptionsDialog.wordStopSegmentationCheckBox: 636 self.cfgParser.set('Morse generation', 'wordDwellSegmentation', str(checkboxNowChecked)); 637 self.cfgParser.set('Morse generation', 'interWordDwellDelay', str(self.morserOptionsDialog.interWordDelaySlider.value()/1000.)); 638 # Enable or disable the inter word delay slider and text box if 639 # word dwell is enabled, and vice versa: 640 if checkboxNowChecked: 641 self.morserOptionsDialog.interWordDelaySlider.setEnabled(True); 642 self.morserOptionsDialog.wordDwellReadoutLineEdit.setEnabled(True); 643 self.morseGenerator.setInterWordDelay(int(self.morserOptionsDialog.wordDwellReadoutLineEdit.text())/1000.0); 644 else: 645 self.morserOptionsDialog.interWordDelaySlider.setEnabled(False); 646 self.morserOptionsDialog.wordDwellReadoutLineEdit.setEnabled(False); 647 # Disable word segmentation: 648 self.morseGenerator.setInterWordDelay(-1); 649 elif checkbox == self.morserOptionsDialog.typeOutputRadioButton: 650 self.cfgParser.set('Output', 'outputDevice', str(OutputType.TYPE)); 651 #************** 652 pass 653 #************** 654 elif checkbox == self.morserOptionsDialog.speechOutputRadioButton: 655 self.cfgParser.set('Output', 'outputDevice', str(OutputType.SPEAK)); 656 #************** 657 pass 658 #************** 659 else: 660 raise ValueError('Unknown checkbox: %s' % str(checkbox));
661 662
663 - def sliderStateChanged(self, slider, newValue):
664 #slider.setToolTip(str(newValue)); 665 #QToolTip.showText(slider.pos(), str(newValue), slider, slider.geometry()) 666 if slider == self.morserOptionsDialog.cursorDecelerationSlider: 667 newValue = newValue/100.0 668 # Update readout: 669 self.morserOptionsDialog.cursorDecelerationReadoutLineEdit.setText(str(newValue)); 670 self.cfgParser.set('Morse generation', 'cursorDeceleration', str(newValue)); 671 self.setCursorDeceleration(newValue); 672 elif slider == self.morserOptionsDialog.keySpeedSlider: 673 # Update readout: 674 self.morserOptionsDialog.keySpeedReadoutLineEdit.setText(str(newValue)); 675 # Speed slider goes from 1 to 6, but QT is set to 676 # have it go from 1 to 60, because fractional intervals 677 # are not allowed. So, scale the read value: 678 newValue = newValue/10.0 679 self.cfgParser.set('Morse generation', 'keySpeed', str(newValue)); 680 self.morseGenerator.setSpeed(newValue); 681 elif slider == self.morserOptionsDialog.interLetterDelaySlider: 682 self.morserOptionsDialog.letterDwellReadoutLineEdit.setText(str(newValue)); 683 valInSecs = newValue/1000.; 684 self.cfgParser.set('Morse generation', 'interLetterDwellDelay', str(valInSecs)); 685 self.morseGenerator.setInterLetterDelay(valInSecs); 686 elif slider == self.morserOptionsDialog.interWordDelaySlider: 687 self.morserOptionsDialog.wordDwellReadoutLineEdit.setText(str(newValue)); 688 valInSecs = newValue/1000.; 689 self.cfgParser.set('Morse generation', 'interWordDwellDelay', str(valInSecs)); 690 self.morseGenerator.setInterWordDelay(valInSecs);
691
692 - def setCursorDeceleration(self, newValue):
693 ''' 694 Change deceleration multiplier for cursor movement inside the 695 rest zone. Value should vary between 0.001 and 1.0 696 @param newValue: multiplier that decelerates cursor. 697 @type newValue: float. 698 ''' 699 self.cursorAcceleration = newValue;
700
701 - def optionsSaveButton(self):
702 try: 703 # Does the config dir already exist? If not 704 # create it: 705 optionsDir = os.path.dirname(self.optionsFilePath); 706 if not os.path.isdir(optionsDir): 707 os.makedirs(optionsDir, 0777); 708 with open(self.optionsFilePath, 'wb') as outFd: 709 self.cfgParser.write(outFd); 710 except IOError as e: 711 self.dialogService.showErrorMsg("Could not save options: %s" % `e`); 712 713 self.morserOptionsDialog.hide();
714
715 - def optionsCancelButton(self):
716 ''' 717 Undo option changes user played with while 718 option box was open. 719 ''' 720 self.morserOptionsDialog.hide(); 721 self.cfgParser.read(self.optionsFilePath); 722 self.initOptionsDialogFromOptions();
723
724 - def buttonEntered(self, buttonObj):
725 if not self.poweredUp: 726 return; 727 if buttonObj == self.dotButton: 728 self.morseGenerator.startMorseSeq(Morse.DOT); 729 elif buttonObj == self.dashButton: 730 self.morseGenerator.startMorseSeq(Morse.DASH); 731 elif buttonObj == self.eowButton: 732 buttonObj.animateClick(); 733 self.outputLetters(' '); 734 elif buttonObj == self.backspaceButton: 735 buttonObj.animateClick(); 736 self.outputBackspace();
737
738 - def buttonExited(self, buttonObj):
739 if buttonObj == self.dotButton: 740 self.morseGenerator.stopMorseSeq(); 741 elif buttonObj == self.dashButton: 742 self.morseGenerator.stopMorseSeq();
743
744 - def eventFilter(self, target, event):
745 746 eventType = event.type(); 747 748 if eventType == QEvent.Enter: 749 # The first time cursor ever enters the Morse 750 # window do the following: 751 if not self.cursorEnteredOnce: 752 # Get the Morse windows X11 window ID saved, 753 # so that we can later activate it whenever 754 # the cursor leaves the Morse window. 755 # First, make the Morse window active: 756 self.virtKeyboard.activateWindow(windowTitle=self.windowTitle); 757 self.virtKeyboard.saveActiveWindowID('morseWinID'); 758 # Also, initialize the keyboard destination: 759 self.virtKeyboard.saveActiveWindowID('keyboardTarget'); 760 self.cursorEnteredOnce = True; 761 762 # Remember X11 window that is active as we 763 # enter the application window, but don't remember 764 # this morse code window, if that was the active one: 765 self.virtKeyboard.saveActiveWindowID('currentActiveWindow'); 766 #*********** 767 # For testing cursor enter/leave focus changes: 768 #currentlyActiveWinID = self.virtKeyboard._getWinIDSafely_('currentActiveWindow'); 769 #morseWinID = self.virtKeyboard._getWinIDSafely_('morseWinID'); 770 #print("Morse win: %s. Curr-active win: %s" % (morseWinID, currentlyActiveWinID)) 771 #*********** 772 if not self.virtKeyboard.windowsEqual('morseWinID', 'currentActiveWindow'): 773 self.virtKeyboard.saveActiveWindowID('keyboardTarget'); 774 #*************** 775 #print("Keyboard target: %s" % self.virtKeyboard._getWinIDSafely_('keyboardTarget')); 776 #*************** 777 self.virtKeyboard.activateWindow(retrievalKey='keyboardTarget'); 778 779 elif eventType == QEvent.Leave: 780 self.virtKeyboard.activateWindow(retrievalKey='morseWinID'); 781 #if (eventType == QEvent.MouseMove) or (event == QHoverEvent): 782 elif eventType == QEvent.MouseMove: 783 if self.constrainCursorInHotZone: 784 self.mouseUnconstrainTimer.stop(); 785 self.handleCursorConstraint(event); 786 # Pass this event on to its destination (rather than filtering it): 787 return False;
788
789 - def moveEvent(self, event):
790 ''' 791 Called when window is repositioned. Need to 792 recompute the cashed global-x positions of 793 the right-side dot button, and the left-side 794 dash button. 795 @param event: move event 796 @type event: QMoveEvent 797 ''' 798 self.computeInnerButtonEdges();
799
800 - def mousePressEvent(self, mouseEvent):
801 802 if (mouseEvent.button() != Qt.LeftButton): 803 return; 804 805 # Re-activate the most recently active X11 window 806 # to ensure the letters are directed to the 807 # proper window, and not this morse window: 808 self.virtKeyboard.activateWindow('keyboardTarget'); 809 if self.cursorInRestZone(mouseEvent.pos()): 810 self.morseGenerator.abortCurrentMorseElement(); 811 812 # Release cursor constraint while mouse button is pressed down. 813 if self.constrainCursorInHotZone: 814 self.stopCursorConstraint(); 815 self.cursorContraintSuspended = True; 816 817 # Pause speed timing, if it's running: 818 self.speedMeasurer.pauseTiming(); 819 820 mouseEvent.accept();
821
822 - def mouseReleaseEvent(self, mouseEvent):
823 824 # If right button is the one that was released, 825 # re-center the mouse to the crosshair. This is 826 # needed with the head mouse tracker to re-calibrate 827 # where it thinks the cursor is located: 828 if mouseEvent.button() == Qt.RightButton: 829 self.morseCursor.setPos(self.centralRestGlobalPos); 830 831 if self.cursorContraintSuspended: 832 self.cursorContraintSuspended = False; 833 self.startCursorConstraint(); 834 # Resume speed timing, (if it was paused: 835 self.speedMeasurer.resumeTiming();
836
837 - def resizeEvent(self, event):
838 newMorseWinRect = self.geometry(); 839 self.cfgParser.set('Appearance', 840 'winGeometry', 841 str(newMorseWinRect.x()) + ',' + 842 str(newMorseWinRect.y()) + ',' + 843 str(newMorseWinRect.width()) + ',' + 844 str(newMorseWinRect.height())); 845 self.optionsSaveButton(); 846 # Update cache of button edge and rest area positions: 847 self.computeInnerButtonEdges();
848
849 - def cursorInRestZone(self, pos):
850 # Button geometries are local, so convert the 851 # given global position: 852 localPos = self.mapFromGlobal(pos); 853 854 dotButtonGeo = self.dotButton.geometry(); 855 dashButtonGeo = self.dashButton.geometry(); 856 return localPos.x() > dotButtonGeo.right() and\ 857 localPos.x() < dashButtonGeo.left() and\ 858 localPos.y() > dotButtonGeo.top() and\ 859 localPos.y() < dotButtonGeo.bottom();
860
861 - def cursorInButton(self, buttonObj, pos, tolerance=0):
862 ''' 863 Return True if given position is within the given button object. 864 An optional positive or negative tolerance is added to the 865 button dimensions. This addition allows for caller to compensate 866 for cursor drift by 'blurring' the true button edges. 867 @param buttonObj: QPushButton or derivative to check. 868 @type buttonObj: QPushButton 869 @param pos: x/y coordinate to test 870 @type pos: QPoint 871 @param tolerance: number of pixels the cursor may be outside the button, yet still be reported as inside. 872 @type tolerance: int 873 ''' 874 # Button geometries are local, so convert the 875 # given global position: 876 buttonGeo = buttonObj.geometry(); 877 globalButtonPos = self.mapToGlobal(QPoint(buttonGeo.x() + tolerance, 878 buttonGeo.y() + tolerance)); 879 globalGeo = QRect(globalButtonPos.x(), globalButtonPos.y(), buttonGeo.width(), buttonGeo.height()); 880 return globalGeo.contains(pos);
881
882 - def handleCursorConstraint(self, mouseEvent):
883 ''' 884 Manages constraining the cursor to vertical/horizontal. Caller is 885 responsible for checking that cursor constraining is wanted. This 886 method assumes so. 887 @param mouseEvent: mouse move event that needs to be addressed 888 @type mouseEvent: QMouseEvent 889 ''' 890 891 try: 892 if self.recentMousePos is None: 893 # Very first time: establish a 'previous' mouse cursor position: 894 self.recentMousePos = mouseEvent.globalPos(); 895 self.headTrackerCursorDrift = 0; 896 return; 897 898 globalPosX = mouseEvent.globalX() 899 globalPosY = mouseEvent.globalY() 900 globalPos = QPoint(globalPosX, globalPosY); 901 localPos = mouseEvent.pos(); 902 903 # If we were already within the dot or dash button 904 # before this new mouse move, and the new mouse 905 # position is still inside that button, then keep 906 # the mouse at the inner edge of the respective button. 907 # ('Inner edge' means facing the resting zone): 908 oldInDot = self.cursorInButton(self.dotButton, self.recentMousePos); 909 oldInDash = self.cursorInButton(self.dashButton, self.recentMousePos); 910 newInDot = self.cursorInButton(self.dotButton, globalPos, tolerance=1); 911 newInDash = self.cursorInButton(self.dashButton, globalPos, tolerance=0); 912 913 oldInButton = oldInDot or oldInDash; 914 newInButton = newInDot or newInDash; 915 916 # Mouse moving within one of the buttons? If 917 # so, keep mouse at the button's inner edge 918 # (facing the rest zone): 919 if newInButton: 920 if newInDot: 921 # The '-1' moves the cursor slightly left into 922 # the Dot button, rather than keeping it right on 923 # the edge. This is to avoid the cursor seemingej 924 # To 'bounce' off the right dot button border back 925 # into the dead zone. The pixel is the drop shadow 926 # on the right side of the dot button. The '+12' places 927 # the hand cursor a bit below the crosshair, so that 928 # color flashes of the crosshair can be seen: 929 self.morseCursor.setPos(self.dotButtonGlobalRight-1, self.centralRestGlobalPos.y() + 12); 930 elif newInDash: 931 self.morseCursor.setPos(self.dashButtonGlobalLeft+1, self.centralRestGlobalPos.y() + 12); 932 return; 933 934 # Only constrain while in rest zone (central empty space), or 935 # inside the dot or dash buttons: 936 if not (self.cursorInRestZone(globalPos) or newInButton): 937 return; 938 939 # If cursor moved while we are constraining motion 940 # vertically or horizontally, enforce that constraint now: 941 if self.currentMouseDirection is not None: 942 if self.currentMouseDirection == Direction.HORIZONTAL: 943 cursorMove = globalPosX - self.recentMousePos.x(); 944 correctedGlobalX = self.recentMousePos.x() + int(cursorMove * self.cursorAcceleration); 945 correctedCurPos = QPoint(correctedGlobalX, self.centralRestGlobalPos.y() + 12); 946 self.recentMousePos.setX(correctedGlobalX); 947 else: 948 cursorMove = globalPosY - self.recentMousePos.y(); 949 correctedGlobalY = self.recentMousePos.y() + int(cursorMove * self.cursorAcceleration); 950 correctedCurPos = QPoint(self.recentMousePos.x(), correctedGlobalPosY); 951 self.recentMousePos.setY(correctedGlobalPosY); 952 self.morseCursor.setPos(correctedCurPos); 953 return; 954 955 # Not currently constraining mouse move. To init, check which 956 # movement larger compared to the most recent position: x or y: 957 # Only constraining horizontally? 958 # Constraint is horizontal, even if there is any initial vertical movement. 959 if (not self.enableConstrainVertical) or abs(globalPosX - self.recentMousePos.x()) > abs(globalPosY - self.recentMousePos.y()): 960 self.currentMouseDirection = Direction.HORIZONTAL; 961 else: 962 self.currentMouseDirection = Direction.VERTICAL; 963 self.recentMousePos = mouseEvent.globalPos(); 964 finally: 965 # Set timer to unconstrain the mouse if it is 966 # not moved for a while (interval is set in __init__()). 967 # If we are not constraining horizontally and vertically 968 # then don't set the timeout: if self.enableConstrainVertical: 969 if self.enableConstrainVertical: 970 self.mouseUnconstrainTimer.setInterval(MorseInput.MOUSE_UNCONSTRAIN_TIMEOUT); 971 self.mouseUnconstrainTimer.start(); 972 return
973
974 - def unconstrainTheCursor(self):
975 # If user is hovering inside the dot or dash button, 976 # keep the hor/vert mouse move constraint going, even 977 # though the timeout of no mouse movement is done: 978 if self.dotButton.underMouse() or self.dashButton.underMouse(): 979 self.mouseUnconstrainTimer.start(); 980 return 981 self.currentMouseDirection = None; 982 self.recentMousePos = None;
983 984 @staticmethod
985 - def letterCompleteNotification(reason, details=''):
986 ''' 987 Called from MorseGenerator when one letter has 988 become available, or when a dwell end-of-letter, 989 or dwell end-of-word was detected. Sends a signal 990 and returns right away. 991 @param reason: indicator whether regular letter, or end of word. 992 @type reason: TimeoutReason 993 ''' 994 MorseInputSignals.getSignal('MorseInputSignals.letterDone').emit(reason, details);
995 996 @QtCore.Slot(int,str)
997 - def deliverInput(self, reason, detail):
998 alpha = self.morseGenerator.getAndRemoveAlphaStr() 999 if reason == TimeoutReason.END_OF_WORD: 1000 alpha += ' '; 1001 self.outputLetters(alpha); 1002 # Give very brief indication that word boundary detected: 1003 self.flashCrosshair(crossHairColor=Crosshairs.YELLOW); 1004 elif reason == TimeoutReason.END_OF_LETTER: 1005 self.outputLetters(alpha); 1006 elif reason == TimeoutReason.BAD_MORSE_INPUT: 1007 self.statusBar.showMessage("Bad Morse input: '%s'" % detail, 4000); # milliseconds
1008
1009 - def outputLetters(self, lettersToSend):
1010 if self.outputDevice == OutputType.TYPE: 1011 for letter in lettersToSend: 1012 # Write to the local ticker tape: 1013 self.tickerTapeAppend(letter); 1014 # Then write to the X11 window in focus: 1015 if letter == '\b': 1016 self.outputBackspace(); 1017 elif letter == '\r': 1018 self.outputNewline(); 1019 else: 1020 #print(letter); 1021 self.virtKeyboard.typeTextToActiveWindow(letter); 1022 elif self.outputDevice == OutputType.SPEAK: 1023 print("Speech not yet implemented.");
1024
1025 - def outputBackspace(self):
1026 self.virtKeyboard.typeControlCharToActiveWindow('BackSpace'); 1027 # Also output to the ticker tape if appropriate: 1028 self.tickerTapeAppend('\b');
1029
1030 - def outputNewline(self):
1031 self.virtKeyboard.typeControlCharToActiveWindow('Linefeed'); 1032 # Also output to the ticker tape if appropriate: 1033 self.tickerTapeAppend('\r');
1034
1035 - def tickerTapeSet(self, text):
1036 self.tickerTapeLineEdit.setText(text);
1037
1038 - def tickerTapeClear(self, dummy):
1039 self.tickerTapeSet('');
1040
1041 - def tickerTapeAppend(self, text):
1042 if not self.useTickerTape: 1043 return; 1044 if text == '\b': 1045 # The backspace() method on QLineEdit doesn't work. 1046 # Maybe because we disallow focus on the widget to 1047 # avoid people thinking they can edit. So work 1048 # around this limitation: 1049 #self.tickerTapeLineEdit.backspace(); 1050 tickerContent = self.tickerTapeLineEdit.text(); 1051 if len(tickerContent) == 0: 1052 return; 1053 self.tickerTapeSet(tickerContent[:-1]); 1054 elif text == '\r': 1055 self.tickerTapeSet(self.tickerTapeLineEdit.text() + '\\n'); 1056 else: 1057 self.tickerTapeSet(self.tickerTapeLineEdit.text() + text);
1058
1059 - def showCrossHair(self, crossHairColor):
1060 if crossHairColor == Crosshairs.CLEAR: 1061 self.crosshairLabel.setPixmap(self.crosshairPixmapClear); 1062 elif crossHairColor == Crosshairs.GREEN: 1063 self.crosshairLabel.setPixmap(self.crosshairPixmapGreen); 1064 elif crossHairColor == Crosshairs.YELLOW: 1065 self.crosshairLabel.setPixmap(self.crosshairPixmapYellow); 1066 elif crossHairColor == Crosshairs.RED: 1067 self.crosshairLabel.setPixmap(self.crosshairPixmapRed); 1068 else: 1069 raise ValueError("Crosshairs are available in clear, green, yellow, and red.") 1070 self.crosshairLabel.setVisible(True);
1071
1072 - def hideCrossHair(self):
1073 self.crosshairLabel.hide();
1074
1075 - def flashCrosshair(self, crossHairColor=Crosshairs.CLEAR):
1076 if self.flashTimer is not None: 1077 return; 1078 self.flashTimer = QTimer(self); 1079 self.flashTimer.setSingleShot(True); 1080 self.flashTimer.setInterval(250); # msecs 1081 self.showCrossHair(crossHairColor); 1082 self.flashTimer.timeout.connect(partial(self.restoreCrosshair, Crosshairs.CLEAR)); 1083 self.flashTimer.start();
1084
1085 - def restoreCrosshair(self, crossHairColor=Crosshairs.CLEAR):
1086 ''' 1087 Show the crosshair with the given color. Stop the flash timer. 1088 This is a timeout method. Used by flashCrosshair(). 1089 @param crossHairColor: 1090 @type crossHairColor: 1091 ''' 1092 self.flashTimer.stop(); 1093 self.flashTimer = None; 1094 self.showCrossHair(crossHairColor);
1095
1096 - def blinkCrosshair(self, doBlink=True, crossHairColor=Crosshairs.CLEAR):
1097 if doBlink: 1098 # If timer already going, don't start a second one: 1099 if self.blinkTimer is not None: 1100 return; 1101 self.blinkTimer = QTimer(self); 1102 self.blinkTimer.setSingleShot(False); 1103 self.blinkTimer.setInterval(500); # msecs 1104 self.crosshairBlinkerOn = True; 1105 self.blinkTimer.timeout.connect(partial(self.toggleBlink, crossHairColor)); 1106 self.blinkTimer.start(); 1107 else: 1108 try: 1109 self.blinkTimer.stop(); 1110 except: 1111 pass 1112 self.blinkTimer = None; 1113 self.showCrossHair(crossHairColor);
1114 1128
1129 - def powerToggled(self):
1130 if self.poweredUp: 1131 self.dotButton.setEnabled(False); 1132 self.dashButton.setEnabled(False); 1133 self.eowButton.setEnabled(False); 1134 self.backspaceButton.setEnabled(False); 1135 self.poweredUp = False; 1136 else: 1137 self.dotButton.setEnabled(True); 1138 self.dashButton.setEnabled(True); 1139 self.eowButton.setEnabled(True); 1140 self.backspaceButton.setEnabled(True); 1141 self.poweredUp = True;
1142
1143 - def exit(self):
1144 self.cleanup(); 1145 QApplication.quit();
1146
1147 - def closeEvent(self, event):
1148 self.cleanup(); 1149 QApplication.quit(); 1150 # Bubble event up: 1151 event.ignore();
1152
1153 - def cleanup(self):
1154 try: 1155 self.morserOptionsDialog.close(); 1156 self.morseGenerator.stopMorseGenerator(); 1157 except: 1158 # Best effort: 1159 pass
1160 1161 if __name__ == '__main__': 1162 1163 app = QApplication(sys.argv); 1164 #QApplication.setStyle(QCleanlooksStyle()) 1165 try: 1166 morser = MorseInput(); 1167 app.exec_(); 1168 morser.exit(); 1169 finally: 1170 try: 1171 fcntl.lockf(MorseInput.morserLockFD, fcntl.LOCK_UN) 1172 except IOError: 1173 print ("Could not release Morser lock.") 1174 sys.exit(); 1175