def __init__(self, textPanel=None, parent=None): super(MarkupManagementUI,self).__init__(parent=parent); self.dialogService = DialogService(); # Fill our space with the UI: self.setupUI(); if textPanel is None: self.textPanel = TextPanel(self.ui.testTextEdit); self.testing = True; else: self.textPanel = textPanel; # Hide the testing text panel self.testTextEdit.hide(); self.testing = False; self.connectWidgets(); self.markupManager = MarkupManagement();
def __init__(self): super(MorseChallenger,self).__init__(); self.PIXELS_TO_MOVE_PER_TIMEOUT = 3 self.ABSOLUTE_MAX_NUM_FLOATERS = 10; self.FLOATER_POINT_SIZE = 20; self.LETTER_CHECKBOX_NUM_COLS = 6; # Number of timeout cycles that detonated # letters stay visible: self.DETONATION_VISIBLE_CYCLES = 4; self.maxNumFloaters = 1; self.lettersToUse = set(); self.floatersAvailable = set(); self.floatersInUse = set(); self.bookeepingAccessLock = threading.Lock(); # Floaters that detonated. Values are number of timeouts # the detonation was visible: self.floatersDetonated = {}; # Used to launch new floaters at random times: self.cyclesSinceLastLaunch = 0; self.dialogService = DialogService(); self.optionsFilePath = os.path.join(os.getenv('HOME'), '.morser/morseChallenger.cfg'); # Load UI for Morse Challenger: relPathQtCreatorFileMainWin = "qt_files/morseChallenger/morseChallenger.ui"; qtCreatorXMLFilePath = self.findFile(relPathQtCreatorFileMainWin); if qtCreatorXMLFilePath is None: raise ValueError("Can't find QtCreator user interface file %s" % relPathQtCreatorFileMainWin); # Make QtCreator generated UI a child of this instance: loadUi(qtCreatorXMLFilePath, self); self.windowTitle = "Morse Challenger"; self.setWindowTitle(self.windowTitle); self.iconDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'icons') self.explosionPixMap = QPixmap(os.path.join(self.iconDir, 'explosion.png')); CommChannel.registerSignals(MorseChallengerSignals); self.connectWidgets(); self.generateLetterCheckBoxes(); self.simultaneousLettersComboBox.addItems(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']); self.generateFloaters(); self.setFocusPolicy(Qt.ClickFocus); self.letterMoveTimer = QTimer(); self.letterMoveTimer.setInterval(self.timerIntervalFromSpeedSlider()); # milliseconds self.letterMoveTimer.setSingleShot(False); self.letterMoveTimer.timeout.connect(self.moveLetters); # Bring options from config file: self.setOptions(); # Styling: self.createColors(); self.setStyleSheet("QWidget{background-color: %s}" % self.lightBlueColor.name()); self.show();
class MarkupManagementUI(QDialog): # Signal used to indicate that a new radio button was selected: markupRequestSig = pyqtSignal(Markup.baseType(), str); # Signal to indicate that the Delete Variation, or the InsertVariation button was pushed: insDelSignal = pyqtSignal(Markup.baseType(), str); # -------------------------- Public Methods -------------------------- def __init__(self, textPanel=None, parent=None): super(MarkupManagementUI,self).__init__(parent=parent); self.dialogService = DialogService(); # Fill our space with the UI: self.setupUI(); if textPanel is None: self.textPanel = TextPanel(self.ui.testTextEdit); self.testing = True; else: self.textPanel = textPanel; # Hide the testing text panel self.testTextEdit.hide(); self.testing = False; self.connectWidgets(); self.markupManager = MarkupManagement(); def getCurrSliderValue(self): ''' Return the slider value of the currently active slider: percentages for Volume/Pitch/Rate, or milliseconds for Silence. @return: slider value @rtype: int ''' return self.valueReadout.value(); def getCurrEmphValue(self): ''' Return currently selected emphasis value radiobutton as an integer. Return None if no emphasis radio button is selected. @return: 0 for 'none', 1 for 'moderate', 2 for 'strong' @int ''' if self.emphasisNoneRadioBt.isChecked(): return MarkupManagement.emphasisStrs[0]; elif self.emphasisModerateRadioBt.isChecked(): return MarkupManagement.emphasisStrs[1]; elif self.emphasisStrongRadioBt.isChecked(): return MarkupManagement.emphasisStrs[2]; return None def setUIForMarkup(self, markupType, val=None): ''' Adjust the markup control panel's UI to match a particular markup type, and emphasis value. For example, an input of Markup.PITCH will hide the silence time slider, and instead expose the percentage slider. If sliderVal is None, the slider's value will remain unchanged. All radio buttons will be unchecked, unless emph is provided. In that case one of the three emphasis radio buttons will be checked. Silence/Volume/Pitch/Rate will still be unchecked. @param markupType: the markup for which the UI is to be adjusted. @type markupType: Markup @param val: optionally, the value to which the visible slider is to be set. @type val: {int | None} ''' if markupType == Markup.EMPHASIS: self.setUIToEmphasis(); elif markupType == Markup.PITCH or\ markupType == Markup.RATE or\ markupType == Markup.VOLUME: self.setUIToPercentages(); elif markupType == Markup.SILENCE: self.setUIToTime(); if markupType == Markup.SILENCE: self.silenceRadioBt.setChecked(True); elif markupType == Markup.RATE: self.rateRadioBt.setChecked(True); elif markupType == Markup.PITCH: self.pitchRadioBt.setChecked(True); elif markupType == Markup.VOLUME: self.volumeRadioBt.setChecked(True); # Depending on the markup type, the value passed in is # different: If markupTYpe is Emphasis, the val is an # emphasis code, in all other cases the value is a slider # value: if val is None: return; # If bad value type, forget it: if type(val) != type(10): return; if markupType == Markup.EMPHASIS: if val == 0: self.emphasisNoneRadioBt.setChecked(True); elif val == 1: self.emphasisModerateRadioBt.setChecked(True); elif val == 2: self.emphasisStrongRadioBt.setChecked(True); # Any other value is illegal. Either way, we're done: return; # Markup is one with a slider value: visibleSlider = self.visibleSlider(); if visibleSlider is not None: sliderVal = min(visibleSlider.maximum(), val); sliderVal = max(visibleSlider.minimum(), val); visibleSlider.setValue(sliderVal); # Echo the new slider value on the digital readout: self.valueReadout.setText(str(sliderVal)); def cursorInMarkup(self): ''' Return a Markup value if cursor of text panel is currently within a markup. Else return None @return: a Markup value if the text cursor sits anywhere within a marked-up text fragment. Else None. @rtype {int | None} ''' return self.markupManager.getMarkupType(self.textPanel.getText(), self.textPanel.getCursorPos()); # -------------------------- Private Methods -------------------------- def setupUI(self): guiPath = os.path.join(os.path.dirname(__file__), '../qtFiles/markupManagement/markupManagement/markupmanagementdialog.ui'); self.ui = python_qt_binding.loadUi(guiPath, self); # Define slightly shorter names to the UI elements: self.deleteButton = self.ui.deleteButton; self.deleteButton.setAutoDefault(False); self.insertVariationButton = self.ui.insertVariationButton; self.insertVariationButton.setAutoDefault(False); self.silenceRadioBt = self.ui.silenceRadioButton; self.rateRadioBt = self.ui.rateRadioButton; self.pitchRadioBt = self.ui.pitchRadioButton; self.volumeRadioBt = self.ui.volumeRadioButton; self.emphasisNoneRadioBt= self.ui.emphasisNoneValRadioButton; self.emphasisModerateRadioBt= self.ui.emphasisModerateValRadioButton; self.emphasisStrongRadioBt= self.ui.emphasisStrongValRadioButton; self.valuePercSlider = self.ui.percentageSlider; self.percSliderMin = self.valuePercSlider.minimum(); self.percSliderMax = self.valuePercSlider.maximum(); self.valueTimeSlider = self.ui.timeSlider; self.timeSliderMin = self.valueTimeSlider.minimum(); self.timeSliderMax = self.valueTimeSlider.maximum(); self.valueReadout = self.ui.valueReadoutLineEdit; self.valueReadout.setValidator(QIntValidator()); self.valueReadout.setText(str(self.percentageSlider.value())); self.percMinLabel = self.ui.percMinLabel; self.percMaxLabel = self.ui.percMaxLabel; self.timeMinLabel = self.ui.timeMinLabel; self.timeMaxLabel = self.ui.timeMaxLabel; self.axisLabel = self.ui.axisLabel; # Initialize UI related states that are changed in response to clicks: self.currMarkupType = Markup.PITCH; self.currentEmph = None self.setUIToPercentages(); def connectWidgets(self): # Radio button connections: self.silenceRadioBt.clicked.connect(partial(self.speechModTypeAction, Markup.SILENCE, emph='none')); self.rateRadioBt.clicked.connect(partial(self.speechModTypeAction, Markup.RATE, emph='none')); self.pitchRadioBt.clicked.connect(partial(self.speechModTypeAction, Markup.PITCH, emph='none')); self.volumeRadioBt.clicked.connect(partial(self.speechModTypeAction, Markup.VOLUME, emph='none')); self.emphasisNoneRadioBt.clicked.connect(partial(self.speechModTypeAction, Markup.EMPHASIS, emph='none')); self.emphasisModerateRadioBt.clicked.connect(partial(self.speechModTypeAction, Markup.EMPHASIS, emph='moderate')); self.emphasisStrongRadioBt.clicked.connect(partial(self.speechModTypeAction, Markup.EMPHASIS, emph='strong')); # Slider connections: self.percentageSlider.valueChanged.connect(self.valueSliderChangedAction); #self.percentageSlider.sliderReleased.connect(partial(self.valueSliderManualSlideFinishedAction, SliderID.PERCENTAGES)); self.timeSlider.valueChanged.connect(self.valueSliderChangedAction); #self.timeSlider.sliderReleased.connect(partial(self.valueSliderManualSlideFinishedAction, SliderID.TIME)); # Value readout and input: self.valueReadout.editingFinished.connect(self.valueReadoutEditingFinishedAction); # Buttons connections: self.insertVariationButton.clicked.connect(partial(self.insOrDelButtonPushedAction, DONT_DELETE)); self.deleteButton.clicked.connect(partial(self.insOrDelButtonPushedAction, DO_DELETE)); # Signal connections: self.markupRequestSig.connect(self.adjustUIToRadioButtonSelection); self.insDelSignal.connect(self.executeMarkupAction); if not self.testing: self.textPanel.mouseClickSignal.connect(self.readyMarkupValChangeUI); @pyqtSlot() def readyMarkupValChangeUI(self): ''' Handler for cursor changes in the text area. Check whether the cursor is within text that is surrounded by markup. If so, find which markup that is, and modify the UI to be appropriate for that markup type (expose the correct slider, etc.). ''' theStr = self.textPanel.getText(); cursorPos = self.textPanel.getCursorPos(); enclosingMarkupType = self.markupManager.getMarkupType(theStr, cursorPos) if enclosingMarkupType is None: return markupValue = self.markupManager.getValue(theStr, cursorPos); self.setUIForMarkup(enclosingMarkupType, markupValue); # Restore the cursor position to where it was (it gets set to 0 by the actions above): self.textPanel.textCursor().setPosition(cursorPos); # **** Necessary ??? def insOrDelButtonPushedAction(self, shouldDelete): ''' Handles variation insert and delete buttons. @param markupType: Type of variation selected (the radio buttons) @type markupType: Markup @param emph: emphasis value (relevant only if Emphasis radio button active). @type emph: String ''' if shouldDelete == DO_DELETE: self.insDelSignal.emit('delete', self.currentEmph); else: self.insDelSignal.emit(self.currMarkupType, self.currentEmph); def speechModTypeAction(self, markupType, emph='none'): ''' Handles all radio button clicks. Raises a signal so that action is taken outside the GUI loop. @param markupType: the radio button type that was activated. @type markupType: Markup @param emph: {'none' | 'moderate' | 'strong'} @type emph: string ''' self.markupRequestSig.emit(markupType, emph); def valueSliderChangedAction(self, newVal): ''' Synchronize the digital input-output slider value box with a newly set slider value. @param newVal: the new slider value @type newVal: int ''' self.valueReadout.setText(str(newVal)); self.updateMarkupValueInTextPanel(newVal); def updateMarkupValueInTextPanel(self, newVal): # If text panel cursor is currently within a markup, # modify that markup's value, unless it's an emphasis, # which has no continuous value: theStr = self.textPanel.getText(); curPos = self.textPanel.getCursorPos(); cursor = self.textPanel.textCursor(); markupType = self.markupManager.getMarkupType(theStr, curPos); if markupType is None: return; # If mark type in the text is different than the mark type whose # radio button is active in the markup control panel, then do nothing: if self.getSelectedMarkupRadioBtn() != markupType: return; newStr = self.markupManager.changeValue(theStr, curPos, newVal); self.textPanel.setText(newStr); # Set the cursor to where it was before replacing the string # in the text panel: cursor.setPosition(curPos) self.textPanel.setTextCursor(cursor); def valueReadoutEditingFinishedAction(self): ''' User typed a value into the value I/O box. Synchronize the slider that is currently visible. ''' # Get value from the readout box. The validator # already ensured that the value is an int: newVal = int(self.valueReadout.text()); if self.valuePercSlider.isVisible(): if newVal > self.percSliderMax: newVal = self.percSliderMax; self.valueReadout.setText(str(newVal)); elif newVal < self.percSliderMin: newVal = self.percSliderMin; self.valueReadout.setText(str(newVal)); self.valuePercSlider.setValue(newVal); elif self.timeSlider.isVisible(): if newVal > self.timeSliderMax: newVal = self.timeSliderMax; self.valueReadout.setText(str(newVal)); elif newVal < self.timeSliderMin: newVal = self.timeSliderMin; self.valueReadout.setText(str(newVal)); self.timeSlider.setValue(newVal); @pyqtSlot(Markup.baseType(), str) def adjustUIToRadioButtonSelection(self, markupType, emph): ''' Signal handler for markupRequestSig. This signal is sent when a radio button is clicked in the UI. The UI is modified to reflect the next possible actions. @param markupType: the radio button that was clicked. @type markupType: Markup @param emph: relevant if Markup == Markup.EMPHASIS: the value of the emphasis value ('none', 'moderate', 'strong'). @type emph: string ''' self.currMarkupType = markupType; self.currentEmph = emph; if markupType == Markup.EMPHASIS: self.setUIToEmphasis(); emphasisValueInt = MarkupManagement.emphasisCodes[emph]; self.updateMarkupValueInTextPanel(emphasisValueInt); elif markupType == Markup.PITCH or\ markupType == Markup.RATE or\ markupType == Markup.VOLUME: self.setUIToPercentages(); elif markupType == Markup.SILENCE: self.setUIToTime(); elif markupType == 'delete': (txtStr, cursorPos, selStart, selEnd) = self.getTextAndSelection(); # Are we to delete enclosing markup? if markupType == 'delete': newStr = self.markupManager.removeMarkup(txtStr, cursorPos); self.textPanel.setText(newStr); return; @pyqtSlot(Markup.baseType(), str) def executeMarkupAction(self, markupType, emph): ''' Handles signal insDelSignal is emitted, which happens when the Delete or Insert Variation button is clicked. Operates on the text panel to surround a selected text piece with a variation markup, or deletes a variation markup the surrounds the current cursor position. @param markupType: the active radio button's identifier, or 'delete' @type markupType: {Markup | 'delete'} ''' # Get a copy of the text panel's current content, the cursor position, # and selection boundaries, if a selection is present: (txtStr, cursorPos, selStart, selEnd) = self.getTextAndSelection(); # Are we to delete enclosing markup? if markupType == 'delete': newStr = self.markupManager.removeMarkup(txtStr, cursorPos); self.textPanel.setText(newStr); return; # All actions other than markup silence, and deletion require a selection: if markupType != Markup.SILENCE: if selStart is None or selEnd is None: self.dialogService.showErrorMsg('Select a piece of text that you wish to modulate.'); return; else: selLen = selEnd - selStart; if markupType == Markup.SILENCE: value = self.valueTimeSlider.value(); # Silence is inserted between words or at the start of a string. # So the cursor position is the marker, not a selection: selStart = cursorPos; selLen = 0; elif markupType != Markup.EMPHASIS: value = self.valuePercSlider.value(); else: value = MarkupManagement.emphasisCodes[emph]; try: newStr = self.markupManager.addMarkup(txtStr, markupType, selStart, length=selLen, value=value); except ValueError as e: self.dialogService.showErrorMsg(`e`); return; self.textPanel.setText(newStr); def setUIToEmphasis(self): ''' Modify UI to show only emphasis choices: ''' self.hideSliders(); def setUIToTime(self): ''' Modify UI to show the time duration slider for defining silence duration: ''' self.valueReadout.validator().setRange(0,MAX_SILENCE); self.valueReadout.setText(str(self.timeSlider.value())); self.showSlider(SliderID.TIME); self.hideSliders(SliderID.PERCENTAGES); def setUIToPercentages(self): ''' Modify UI to show the percentage slider relevant to volume, rate, and pitch markups. ''' self.valueReadout.validator().setRange(-100,100); self.valueReadout.setText(str(self.valuePercSlider.value())); self.showSlider(SliderID.PERCENTAGES); self.hideSliders(SliderID.TIME); def hideSliders(self, sliderID=None): ''' Hide one or both percentage or time sliders. @param sliderID: which slider to hide. None if hide both @type sliderID: {SliderID | None} ''' if sliderID == SliderID.PERCENTAGES or sliderID is None: self.percentageSlider.hide(); self.percMinLabel.hide(); self.percMaxLabel.hide(); if sliderID == SliderID.TIME or sliderID is None: self.timeSlider.hide(); self.timeMinLabel.hide(); self.timeMaxLabel.hide(); if sliderID is None: self.axisLabel.hide(); self.valueReadout.hide(); def showSlider(self, sliderID): ''' Reveal the specified slider @param sliderID: the slider to reveal (never both) @type sliderID: SliderID ''' if sliderID == SliderID.PERCENTAGES: self.valuePercSlider.show(); self.percMinLabel.show(); self.percMaxLabel.show(); elif sliderID == SliderID.TIME: self.timeSlider.show(); self.timeMinLabel.show(); self.timeMaxLabel.show(); self.axisLabel.show(); self.valueReadout.show(); def visibleSlider(self): ''' Return the slider object that is currently visible to the user. @return: slider object (percentage or time slider). If both sliders are currently hidden, return None. @rtype: {QSlider | None} ''' if self.valuePercSlider.isVisible(): return self.valuePercSlider; elif self.valueTimeSlider.isVisible(): return self.valueTimeSlider; else: return None def getSelectedMarkupRadioBtn(self): ''' Return the Markup type whose radio button is selected in the markup control panel. If none selected, return None @return: the Markup type whose radio button is selected @rtype: {Markup | None} ''' if self.silenceRadioBt.isChecked(): return Markup.SILENCE; elif self.rateRadioBt.isChecked(): return Markup.RATE; elif self.pitchRadioBt.isChecked(): return Markup.PITCH; elif self.volumeRadioBt.isChecked(): return Markup.VOLUME; elif self.emphasisNoneRadioBt.isChecked() or self.emphasisModerateRadioBt.isChecked() or self.emphasisStrongRadioBt.isChecked(): return Markup.EMPHASIS; return None; def getTextAndSelection(self): ''' Returns a four-tuple string, cursor position, start index, and end index. The string is a copy of the current self.textPanel object. The two index numbers are the start (inclusive), and end (exclusive) of the current text selection. If no text is selected, return (text, None, None). @return: triplet string, and start/end of text selection. @rtype: (String,int,int,int) or (String,int,None,None) ''' txtCursor = self.textPanel.textCursor(); curPos = txtCursor.position(); selStart = txtCursor.selectionStart(); selEnd = txtCursor.selectionEnd(); if selStart == selEnd: selStart = selEnd = None; txt = self.textPanel.getText(); return (txt,curPos,selStart,selEnd); def shutdown(self): sys.exit();
class MorseChallenger(QMainWindow): def __init__(self): super(MorseChallenger,self).__init__(); self.PIXELS_TO_MOVE_PER_TIMEOUT = 3 self.ABSOLUTE_MAX_NUM_FLOATERS = 10; self.FLOATER_POINT_SIZE = 20; self.LETTER_CHECKBOX_NUM_COLS = 6; # Number of timeout cycles that detonated # letters stay visible: self.DETONATION_VISIBLE_CYCLES = 4; self.maxNumFloaters = 1; self.lettersToUse = set(); self.floatersAvailable = set(); self.floatersInUse = set(); self.bookeepingAccessLock = threading.Lock(); # Floaters that detonated. Values are number of timeouts # the detonation was visible: self.floatersDetonated = {}; # Used to launch new floaters at random times: self.cyclesSinceLastLaunch = 0; self.dialogService = DialogService(); self.optionsFilePath = os.path.join(os.getenv('HOME'), '.morser/morseChallenger.cfg'); # Load UI for Morse Challenger: relPathQtCreatorFileMainWin = "qt_files/morseChallenger/morseChallenger.ui"; qtCreatorXMLFilePath = self.findFile(relPathQtCreatorFileMainWin); if qtCreatorXMLFilePath is None: raise ValueError("Can't find QtCreator user interface file %s" % relPathQtCreatorFileMainWin); # Make QtCreator generated UI a child of this instance: loadUi(qtCreatorXMLFilePath, self); self.windowTitle = "Morse Challenger"; self.setWindowTitle(self.windowTitle); self.iconDir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'icons') self.explosionPixMap = QPixmap(os.path.join(self.iconDir, 'explosion.png')); CommChannel.registerSignals(MorseChallengerSignals); self.connectWidgets(); self.generateLetterCheckBoxes(); self.simultaneousLettersComboBox.addItems(['1', '2', '3', '4', '5', '6', '7', '8', '9', '10']); self.generateFloaters(); self.setFocusPolicy(Qt.ClickFocus); self.letterMoveTimer = QTimer(); self.letterMoveTimer.setInterval(self.timerIntervalFromSpeedSlider()); # milliseconds self.letterMoveTimer.setSingleShot(False); self.letterMoveTimer.timeout.connect(self.moveLetters); # Bring options from config file: self.setOptions(); # Styling: self.createColors(); self.setStyleSheet("QWidget{background-color: %s}" % self.lightBlueColor.name()); self.show(); def connectWidgets(self): self.speedHSlider.valueChanged.connect(self.speedChangedAction); self.startPushButton.clicked.connect(self.startAction); self.stopPushButton.clicked.connect(self.stopAction); self.simultaneousLettersComboBox.currentIndexChanged.connect(self.maxNumSimultaneousFloatersAction); CommChannel.getSignal('MorseChallengerSignals.focusLostInadvertently').connect(self.reassertWindowFocus); def createColors(self): self.grayBlueColor = QColor(89,120,137); # Letter buttons self.lightBlueColor = QColor(206,230,243); # Background self.darkGray = QColor(65,88,101); # Central buttons self.wordListFontColor = QColor(62,143,185); # Darkish blue. self.purple = QColor(147,124,195); # Gesture button pressed def generateLetterCheckBoxes(self): self.lettersToCheckBoxes = {}; self.checkBoxesToLetters = {}; letters = sorted(codeKey.values()); numRows = (len(letters) % self.LETTER_CHECKBOX_NUM_COLS) + 1; col = 0; row = 0; letterIndex = 0; while(True): letter = letters[letterIndex]; checkbox = QCheckBox(); self.lettersToCheckBoxes[letter] = checkbox; self.checkBoxesToLetters[checkbox] = letter; checkbox.setText(str(letter)); checkbox.toggled.connect(partial(self.letterCheckAction, checkbox)); self.letterCheckboxGridLayout.addWidget(checkbox, row, col); col += 1; if (col >= self.LETTER_CHECKBOX_NUM_COLS): col = 0; row += 1; letterIndex += 1; if letterIndex >= len(letters): break; def generateFloaters(self): for i in range(self.ABSOLUTE_MAX_NUM_FLOATERS): floaterLabel = QLabel(); font = QtGui.QFont() font.setFamily("Courier") font.setFixedPitch(True) font.setPointSize(self.FLOATER_POINT_SIZE); floaterLabel.setFont(font); self.floatersAvailable.add(floaterLabel); def letterCheckAction(self, checkbox, checkedOrNot): changedLetter = self.checkBoxesToLetters[checkbox]; configedLetters = self.cfgParser.get('Main', 'letters'); if checkedOrNot: self.lettersToUse.add(self.checkBoxesToLetters[checkbox]); configedLetters += changedLetter; else: self.lettersToUse.discard(self.checkBoxesToLetters[checkbox]); configedLetters.replace(changedLetter, ""); self.cfgParser.set('Main', 'letters', configedLetters); self.saveOptions(); def startAction(self): self.launchFloaters(1); self.letterMoveTimer.start(); def stopAction(self): self.letterMoveTimer.stop(); floatersInUseCopy = self.floatersInUse.copy(); for floater in floatersInUseCopy: self.decommissionFloater(floater); def maxNumSimultaneousFloatersAction(self, newNum): # New Combo box picked: 0-based: self.maxNumFloaters = newNum + 1; try: self.cfgParser.set('Main','numLettersTogether', str(newNum)); self.saveOptions(); except AttributeError: # At startup cfgParser won't exist yet. Ignore: pass def speedChangedAction(self, newSpeed): self.letterMoveTimer.setInterval(self.timerIntervalFromSpeedSlider(newSpeed)); # msec. self.cfgParser.set('Main','letterSpeed', str(newSpeed)); self.saveOptions(); def keyPressEvent(self, keyEvent): letter = keyEvent.text(); self.letterLineEdit.setText(letter); matchingFloaters = {}; # Find all active floaters that have the pressed key's letter: for floater in self.floatersInUse: if floater.text() == letter: floaterY = floater.y(); try: matchingFloaters[floaterY].append(floater); except KeyError: matchingFloaters[floaterY] = [floater]; if len(matchingFloaters) == 0: self.activateWindow(); self.setFocus(); return; # Find the lowest ones: lowestY = self.projectionScreenWidget.y(); yPositions = matchingFloaters.keys(); for floater in matchingFloaters[max(yPositions)]: self.decommissionFloater(floater); self.activateWindow(); self.setFocus(); def focusInEvent(self, event): ''' Ensure that floaters are always on top of the app window, even if user changes speed slider, checkmarks, etc.: @param event: @type event: ''' self.raiseAllFloaters(); def resizeEvent(self, event): newWinRect = self.geometry(); self.cfgParser.set('Appearance', 'winGeometry', str(newWinRect.x()) + ',' + str(newWinRect.y()) + ',' + str(newWinRect.width()) + ',' + str(newWinRect.height())); self.saveOptions(); def raiseAllFloaters(self): for floater in self.floatersInUse: floater.raise_(); for floater in self.floatersDetonated.keys(): floater.raise_(); def decommissionFloater(self, floaterLabel): floaterLabel.setHidden(True); # Just in case: protect removal, in case caller # passes in an already decommissioned floater: try: self.floatersInUse.remove(floaterLabel); except KeyError: pass # Adding a floater twice is not an issue, # b/c floatersAvailable is a set: self.floatersAvailable.add(floaterLabel); def timerIntervalFromSpeedSlider(self, newSpeed=None): if newSpeed is None: newSpeed = self.speedHSlider.value(); return max(500 - newSpeed, 20); #msec def randomLetters(self, numLetters): if len(self.lettersToUse) == 0: self.dialogService.showErrorMsg("You must turn on a checkmark for at least one letter."); return None; lettersToDeploy = []; for i in range(numLetters): letter = random.sample(self.lettersToUse, 1); lettersToDeploy.extend(letter); return lettersToDeploy; def moveLetters(self): self.cyclesSinceLastLaunch += 1; # Did floater detonate during previous timeout?: detonatedFloaters = self.floatersDetonated.keys(); for detonatedFloater in detonatedFloaters: self.floatersDetonated[detonatedFloater] += 1; if self.floatersDetonated[detonatedFloater] > self.DETONATION_VISIBLE_CYCLES: self.decommissionFloater(detonatedFloater); del self.floatersDetonated[detonatedFloater]; remainingFloaters = self.floatersDetonated.keys(); for floaterLabel in self.floatersInUse: if floaterLabel in remainingFloaters: continue; geo = floaterLabel.geometry(); newY = geo.y() + self.PIXELS_TO_MOVE_PER_TIMEOUT; savedHeight = geo.height(); if newY > self.height(): newY = self.height(); #******** self.detonate(floaterLabel); geo.setY(newY) geo.setHeight(savedHeight); floaterLabel.setGeometry(geo); # Done advancing each floater. Is it time to start a new floater? numFloatersToLaunch = self.maxNumFloaters - len(self.floatersInUse) + len(self.floatersDetonated); if numFloatersToLaunch > 0: # Use the following commented lines if you want the launching of # new floaters to be random, e.g. between 2 and 10 timeout intervals: # if self.cyclesSinceLastLaunch > random.randint(2,10): # self.launchFloaters(self.maxNumFloaters - len(self.floatersInUse)); self.launchFloaters(numFloatersToLaunch); # Launching floaters deactivates the main window, thereby losing the keyboard focus. # We can't seem to reassert that focus within this timer interrupt service routine. # So, issue a signal that will do it after return: CommChannel.getSignal('MorseChallengerSignals.focusLostInadvertently').emit(); def launchFloaters(self, numFloaters): newLetters = self.randomLetters(numFloaters); if newLetters is None: return; for letter in newLetters: # Pick a random horizontal location, at least 3 pixels in # from the left edge, and at most 3 pixels back from the right edge: screenGeo = self.projectionScreenWidget.geometry(); appWinGeo = self.geometry(); appWinX = appWinGeo.x(); # Random number among all the application window's X coordinates: xLoc = random.randint(appWinX + 3, appWinX + screenGeo.width() - 3); yLoc = appWinGeo.y(); self.getFloaterLabel(letter, xLoc, yLoc); self.cyclesSinceLastLaunch = 0; def getFloaterLabel(self, letter, x, y): try: label = self.floatersAvailable.pop(); label.clear(); except KeyError: return None; self.floatersInUse.add(label); label.setText(letter); label.move(x,y); label.setHidden(False); def detonate(self, floaterLabel): # Floater was detonated zero cycles ago: self.floatersDetonated[floaterLabel] = 0; floaterLabel.clear(); floaterLabel.setPixmap(self.explosionPixMap); def findFile(self, path, matchFunc=os.path.isfile): if path is None: return None for dirname in sys.path: candidate = os.path.join(dirname, path) if matchFunc(candidate): return candidate return None; def reassertWindowFocus(self): self.window().activateWindow(); self.raise_(); self.raiseAllFloaters(); def setOptions(self): self.optionsDefaultDict = { 'letters' : "", 'letterSpeed' : str(10), 'numLettersTogether' : str(0), 'winGeometry' : '100,100,700,560', } self.cfgParser = ConfigParser.SafeConfigParser(self.optionsDefaultDict); self.cfgParser.add_section('Main'); self.cfgParser.add_section('Appearance'); self.cfgParser.read(self.optionsFilePath); mainWinGeometry = self.cfgParser.get('Appearance', 'winGeometry'); # Get four ints from the comma-separated string of upperLeftX, upperLeftY, # Width,Height numbers: try: nums = mainWinGeometry.split(','); self.setGeometry(QRect(int(nums[0].strip()),int(nums[1].strip()),int(nums[2].strip()),int(nums[3].strip()))); except Exception as e: self.dialogService.showErrorMsg("Could not set window size; config file spec not grammatical: %s. (%s" % (mainWinGeometry, `e`)); letterSpeed = self.cfgParser.getint('Main', 'letterSpeed'); self.speedHSlider.setValue(letterSpeed); #self.speedChangedAction(letterSpeed); self.letterMoveTimer.setInterval(self.timerIntervalFromSpeedSlider()); # milliseconds numLettersTogether = self.cfgParser.getint('Main', 'numLettersTogether'); self.simultaneousLettersComboBox.setCurrentIndex(numLettersTogether); lettersToUse = self.cfgParser.get('Main', 'letters'); for letter in lettersToUse: self.lettersToCheckBoxes[letter].setChecked(True); def saveOptions(self): try: # Does the config dir already exist? If not # create it: optionsDir = os.path.dirname(self.optionsFilePath); if not os.path.isdir(optionsDir): os.makedirs(optionsDir, 0777); with open(self.optionsFilePath, 'wb') as outFd: self.cfgParser.write(outFd); except IOError as e: self.dialogService.showErrorMsg("Could not save options: %s" % `e`); def exit(self): QApplication.quit(); def closeEvent(self, event): QApplication.quit(); # Bubble event up: event.ignore();