| Trees | Indices | Help |
|---|
|
|
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;
63
67
73
77
81
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
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
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
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
267
271
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
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
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
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
385
386
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
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
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)
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
495 if self.speedMeasureWidget.isVisible():
496 self.expandMorePanel(PanelExpansion.LESS);
497 else:
498 self.expandMorePanel(PanelExpansion.MORE);
499
502
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
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
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
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
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
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
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
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
737
743
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
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
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
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
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
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
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
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
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
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)
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
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
1026 self.virtKeyboard.typeControlCharToActiveWindow('BackSpace');
1027 # Also output to the ticker tape if appropriate:
1028 self.tickerTapeAppend('\b');
1029
1031 self.virtKeyboard.typeControlCharToActiveWindow('Linefeed');
1032 # Also output to the ticker tape if appropriate:
1033 self.tickerTapeAppend('\r');
1034
1037
1039 self.tickerTapeSet('');
1040
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
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
1074
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
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
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
1116 '''
1117 If alternates between crosshair on and off.
1118 This is a timeout method. Used by blinkCrosshair()
1119 @param crossHairColor:
1120 @type crossHairColor:
1121 '''
1122 if self.crosshairBlinkerOn:
1123 self.hideCrossHair();
1124 self.crosshairBlinkerOn = False;
1125 else:
1126 self.showCrossHair(crossHairColor);
1127 self.crosshairBlinkerOn = True;
1128
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
1146
1152
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
| Trees | Indices | Help |
|---|
| Generated by Epydoc 3.0.1 on Thu Feb 21 11:48:07 2013 | http://epydoc.sourceforge.net |