class Quiz(QFrame): def __init__(self, options, parent=None): super(Quiz, self).__init__(parent) self.options = options """Session Info""" self.status = QFrame() #session message self.status.message = QLabel(u'') #achievements self.status.achievements = Achievements() self.status.info = QLabel(u'') self.status.progress = QProgressBar() self.status.layout = QVBoxLayout() #layout self.status.layout.addWidget(self.status.info) self.status.layout.addWidget(self.status.progress) self.status.layout.addWidget(self.status.message) self.status.setLayout(self.status.layout) #mouse event filter self.status.filter = StatusFilter(self.status) self.status.setAttribute(Qt.WA_Hover, True) self.status.installEventFilter(self.status.filter) """Items Info""" self.info = QFrame() self.info.reading = QLabel(u'') self.info.item = QLabel(u'') self.info.components = QLabel(u'') separator_one = QFrame() separator_one.setFrameShape(QFrame.HLine) separator_one.setFrameShadow(QFrame.Sunken) separator_two = QFrame() separator_two.setFrameShape(QFrame.HLine) separator_two.setFrameShadow(QFrame.Sunken) self.info.layout = QVBoxLayout() self.info.layout.addWidget(self.info.reading) self.info.layout.addWidget(separator_one) self.info.layout.addWidget(self.info.item) self.info.layout.addWidget(separator_two) self.info.layout.addWidget(self.info.components) self.info.setLayout(self.info.layout) """Verbose Info""" self.allInfo = QFrame() self.allInfo.layout = QGridLayout() self.allInfo.setLayout(self.allInfo.layout) #the rest is (should be) generated on the fly """Kanji info""" self.kanjiInfo = QFrame() self.kanjiInfo.layout = QVBoxLayout() self.kanjiInfo.info = QLabel(u'') self.kanjiInfo.layout.addWidget(self.kanjiInfo.info) self.kanjiInfo.setLayout(self.kanjiInfo.layout) """Kanji groups""" self.kanjiGroups = QFrame() self.kanjiGroups.layout = QVBoxLayout() self.kanjiGroups.info = QLabel(u'') self.kanjiGroups.layout.addWidget(self.kanjiGroups.info) self.kanjiGroups.setLayout(self.kanjiGroups.layout) """Global Flags""" #self.correct = False """Quiz Dialog""" self.filter = Filter() #### visual components ### self.countdown = QProgressBar() self.sentence = QLabel(u'') self.var_1st = QPushButton(u'') self.var_2nd = QPushButton(u'') self.var_3rd = QPushButton(u'') self.var_4th = QPushButton(u'') self.answered = QPushButton(u'') self.answered.hide() ### layouts #### self.layout_vertical = QVBoxLayout() #main self.layout_horizontal = QHBoxLayout() #buttons self.layout_horizontal.addWidget(self.var_1st) self.layout_horizontal.addWidget(self.var_2nd) self.layout_horizontal.addWidget(self.var_3rd) self.layout_horizontal.addWidget(self.var_4th) self.layout_vertical.addWidget(self.countdown) self.layout_vertical.addWidget(self.sentence) self.layout_vertical.addLayout(self.layout_horizontal) self.layout_horizontal.addWidget(self.answered) self.setLayout(self.layout_vertical) ### utility components ### self.trayIcon = QSystemTrayIcon(self) self.trayMenu = QMenu() self.gifLoading = QMovie('../res/cube.gif') self.gifLoading.frameChanged.connect(self.updateTrayIcon) ### initializing ### self.initializeResources() ### timers ### self.nextQuizTimer = QTimer() self.nextQuizTimer.setSingleShot(True) self.nextQuizTimer.timeout.connect(self.showQuiz) self.countdownTimer = QTimer() self.countdownTimer.setSingleShot(True) self.countdownTimer.timeout.connect(self.timeIsOut) self.trayUpdater = None #self.trayUpdater = RepeatTimer(1.0, self.updateTrayTooltip, self.options.getRepetitionInterval() * 60) self.remaining = 0 """Start!""" if self.options.isQuizStartingAtLaunch(): self.waitUntilNextTimeslot() self.trayIcon.setToolTip('Quiz has started automatically!') self.pauseAction.setText('&Pause') self.trayIcon.showMessage('Loading complete! (took ~'+ str(self.loadingTime.seconds) + ' seconds) Quiz underway.', 'Lo! Quiz already in progress!' + self.loadingStatus, QSystemTrayIcon.MessageIcon.Warning, 10000) else: self.trayIcon.setToolTip('Quiz is not initiated!') self.trayIcon.showMessage('Loading complete! (took ~' + str(self.loadingTime.seconds) + ' seconds) Standing by.', 'Quiz has not started yet! If you wish, you could start it manually or enable autostart by default.' + self.loadingStatus, QSystemTrayIcon.MessageIcon.Information, 10000 ) self.setWindowIcon(QIcon(PATH_TO_RES + ICONS + 'suzu.png')) """Test calls here:""" ### ... ### #self.connect(self.hooker, SIGNAL('noQdict'), self.noQdict) self.gem = self.saveGeometry() def startUpdatingTrayTooltip(self): self.remaining = self.nextQuizTimer.interval() if self.trayUpdater is not None and self.trayUpdater.isAlive(): self.trayUpdater.cancel() self.trayUpdater = RepeatTimer(1.0, self.updateTrayTooltip, self.options.getRepetitionInterval() * 60) self.trayUpdater.start() def updateTrayTooltip(self): self.remaining -= UPDATE_FREQ self.trayIcon.setToolTip('Next quiz in ' + (str(self.remaining/UPDATE_FREQ) + ' seconds')) # def noQdict(self): # self.showSessionMessage('Nope, cannot show quick dictionary during actual quiz.') #################################### # Initialization procedures # #################################### def initializeResources(self): """Initialize Options""" # self.options = Options() self.loadingStatus = u'' self.qload = QuickLoad(self.options) if self.options.isLoadingOnStart(): self.qload.exec_() """Pre-initialization""" self.animationTimer = () self.progressTimer = () self.grid_layout =() """Initialize Statistics""" self.stats = Stats() """Config Here""" self.initializeComposition() self.initializeComponents() self.setMenus() self.trayIcon.show() #self.startTrayLoading() """"Initialize Dictionaries (will take a some time!)""" time_start = datetime.now() self.trayIcon.showMessage('Loading...', 'Initializing dictionaries', QSystemTrayIcon.MessageIcon.Information, 20000 ) # kanji composition # if self.options.isLoadingRadk(): self.rdk = RadkDict() else: self.loadingStatus += '--> Radikt disabled!\n' # edict dictionary if self.options.isLoadingEdict(): edict_file = resource_filename('cjktools_data', 'dict/je_edict') self.edict = auto_format.load_dictionary(edict_file) else: self.edict = None self.loadingStatus += '--> Edict disabled!\n' # kanjidict dictionary # if self.options.isLoadingKdict(): self.kjd = kanjidic.Kanjidic() else: self.kjd = None self.loadingStatus += '--> Kanjidict disabled!\n' # Kanji.Odyssey groups # self.groups = KanjiGrouper() if self.options.isLoadingGroups(): self.groups.loadgroupsFromDump() else: self.loadingStatus += '--> Kanji.Odyssey disabled!\n' """Initializing srs system""" self.trayIcon.showMessage('Loading...', 'Initializing databases', QSystemTrayIcon.MessageIcon.Information, 20000 ) self.srs = srsScheduler() if self.options.isLoadingDb(): self.srs.initializeCurrentSession(self.options.getQuizMode(), self.options.getSessionSize()) else: self.loadingStatus += '--> Database disabled!\n' """Jmdict lookup""" self.jmdict = DictionaryLookup() if self.options.isLoadingJmdict(): self.jmdict.loadJmdictFromDumpRegex() self.jmdict.joinTables() else: self.loadingStatus += '--> Jmdict disabled!\n' """Manual add dialog""" self.manualAddDialog = ManualAdd(self.srs.db) if self.loadingStatus != '': self.loadingStatus = '\n\n' + self.loadingStatus time_end = datetime.now() self.loadingTime = time_end - time_start #################################### # Composition and appearance # #################################### def initializeComposition(self): """Main Dialog""" self.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.setFocusPolicy(Qt.StrongFocus) self.setFrameStyle(QFrame.StyledPanel | QFrame.Raised) #NB: This font will be used in buttons self.setFont(QFont(Fonts.TukusiMyoutyouProLB, self.options.getQuizFontSize())) desktop = QApplication.desktop().screenGeometry() self.setGeometry(QRect(desktop.width() - H_INDENT, desktop.height() - V_INDENT, D_WIDTH, D_HEIGHT)) self.setStyleSheet("QWidget { background-color: rgb(252, 252, 252); }") """Info dialog""" self.info.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.info.setFrameStyle(QFrame.StyledPanel | QFrame.Raised) self.info.setGeometry(QRect(desktop.width() - H_INDENT - I_WIDTH - I_INDENT, desktop.height() - V_INDENT, I_WIDTH, I_HEIGHT)) self.info.setStyleSheet("QWidget { background-color: rgb(252, 252, 252); }") """Verbose info dialog""" self.allInfo.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.allInfo.setFrameStyle(QFrame.StyledPanel | QFrame.Raised) self.allInfo.setGeometry(QRect(desktop.width() - H_INDENT - I_WIDTH - I_INDENT, desktop.height() - V_INDENT, I_WIDTH, I_HEIGHT)) self.allInfo.setStyleSheet("QWidget { background-color: rgb(252, 252, 252); }") """Session message""" self.status.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.status.setFrameStyle(QFrame.StyledPanel | QFrame.Raised) self.status.setGeometry(QRect(desktop.width() - H_INDENT, desktop.height() - V_INDENT - S_HEIGHT - S_INDENT - S_CORRECTION, S_WIDTH, S_HEIGHT)) self.status.setMinimumSize(S_WIDTH, S_HEIGHT) # self.status.setMinimumWidth(S_WIDTH) self.status.setStyleSheet("QWidget { background-color: rgb(252, 252, 252); }") self.setMask(roundCorners(self.rect(),5)) # self.status.setMask(roundCorners(self.status.rect(),5)) #self.info.setMask(roundCorners(self.info.rect(),5)) #self.allInfo.setMask(roundCorners(self.allInfo.rect(),5)) """Kanji info""" self.kanjiInfo.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.kanjiInfo.setFrameStyle(QFrame.StyledPanel | QFrame.Raised) self.kanjiInfo.setGeometry(QRect(desktop.width() - H_INDENT - K_WIDTH - K_INDENT, desktop.height() - V_INDENT, K_WIDTH, K_HEIGHT)) self.kanjiInfo.setStyleSheet("QWidget { background-color: rgb(252, 252, 252); }") """Kanji groups""" self.kanjiGroups.setWindowFlags(Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint) self.kanjiGroups.setFrameStyle(QFrame.StyledPanel | QFrame.Raised) self.kanjiGroups.setGeometry(QRect(desktop.width() - H_INDENT - G_WIDTH - G_INDENT, desktop.height() - V_INDENT, G_WIDTH, G_HEIGHT)) self.kanjiGroups.setStyleSheet("QWidget { background-color: rgb(250, 250, 250); }") # self.setMask(roundCorners(self.rect(),5)) # self.status.setMask(roundCorners(self.status.rect(),5)) def initializeComponents(self): self.countdown.setMaximumHeight(6) self.countdown.setRange(0, self.options.getCountdownInterval() * 100) self.countdown.setTextVisible(False) self.countdown.setStyleSheet("QProgressbar { background-color: rgb(255, 255, 255); }") #self.setFont(QFont(Fonts.SyoutyouProEl, 40))#self.options.getQuizFontSize())) self.sentence.setAlignment(Qt.AlignmentFlag.AlignCenter) #self.sentence.setFont(QFont(Fonts.HiragiNoMarugotoProW4, self.options.getSentenceFontSize())) self.sentence.setFont(QFont(self.options.getSentenceFont(), self.options.getSentenceFontSize())) self.sentence.setWordWrap(True) self.trayIcon.setIcon(QIcon(PATH_TO_RES + TRAY + 'active.png')) self.status.message.setFont(QFont('Cambria', self.options.getMessageFontSize())) self.status.layout.setAlignment(Qt.AlignCenter) self.status.message.setWordWrap(False) self.status.message.setAlignment(Qt.AlignCenter) self.status.layout.setMargin(0) self.status.info.setHidden(True) self.status.progress.setHidden(True) self.status.progress.setMaximumHeight(10) self.status.progress.setRange(0, self.status.achievements.threshold) self.status.layout.setAlignment(Qt.AlignCenter) self.status.info.setAlignment(Qt.AlignCenter) self.status.info.setFont(QFont(Fonts.RyuminPr5, 13)) self.status.info.setWordWrap(False) self.status.gem = self.status.saveGeometry() self.info.item.setFont(QFont(Fonts.HiragiNoMyoutyouProW3, 36)) self.info.reading.setFont(QFont(Fonts.HiragiNoMyoutyouProW3, 16)) self.info.components.setFont((QFont(Fonts.HiragiNoMyoutyouProW3, 14))) #self.info.item.setWordWrap(True) self.info.components.setWordWrap(True) #self.info.layout.setAlignment(Qt.AlignCenter) self.info.layout.setMargin(0) #self.info.layout.setSizeConstraint(self.info.layout.SetFixedSize) #NB: would work nice, if the anchor point was in right corner self.info.reading.setAlignment(Qt.AlignCenter) self.info.item.setAlignment(Qt.AlignCenter) self.info.components.setAlignment(Qt.AlignCenter) #self.info.setLayoutDirection(Qt.RightToLeft) self.info.reading.setStyleSheet("QLabel { color: rgb(155, 155, 155); }") self.info.components.setStyleSheet("QLabel { color: rgb(100, 100, 100); }") self.kanjiInfo.info.setFont(QFont(Fonts.MSMyoutyou, 14.5)) self.kanjiInfo.info.setAlignment(Qt.AlignCenter) self.kanjiInfo.info.setWordWrap(True) self.kanjiInfo.layout.setMargin(0) self.kanjiGroups.info.setFont(QFont(Fonts.MSMyoutyou, 18.5)) self.kanjiGroups.info.setAlignment(Qt.AlignCenter) self.kanjiGroups.info.setWordWrap(True) self.kanjiGroups.layout.setMargin(0) #NB: ... self.answered.setMaximumWidth(D_WIDTH) self.answered.setFont(QFont('Calibri', 11)) #################################### # Updating content # #################################### def updateContent(self): """Resetting multi-label sentence""" if self.grid_layout != (): for i in range(0, self.grid_layout.count()): self.grid_layout.itemAt(i).widget().hide() self.layout_vertical.removeItem(self.grid_layout) self.grid_layout.setParent(None) self.update() if self.sentence.isHidden(): self.sentence.show() self.showButtonsQuiz() """Getting actual content""" self.srs.getNextItem() example = self.srs.getCurrentExample() # checking for no example case if example is None: self.manualAddDialog.setProblemKanji(self.srs.getCurrentItemKanji()) done = self.manualAddDialog.exec_() if done == 0: self.updateContent() elif done == 1: self.updateContent() else: pass else: example = example.replace(self.srs.getWordFromExample(), u"<font color='blue'>" + self.srs.getWordFromExample() + u"</font>") # checking sentence length if len(self.srs.currentExample.sentence) > SENTENCE_MAX: self.sentence.setFont(QFont(self.options.getSentenceFont(), MIN_FONT_SIZE)) else: self.sentence.setFont(QFont(self.options.getSentenceFont(), self.options.getSentenceFontSize())) #temporary debug info: # print len(example), self.sentence.font() self.sentence.setText(example) readings = self.srs.getQuizVariants() changeFont = False for item in readings: if len(item) > BUTTON_KANA_MAX : changeFont = True try: for i in range(0, self.layout_horizontal.count()): if i > 3: break self.layout_horizontal.itemAt(i).widget().setText(u'') if changeFont: self.layout_horizontal.itemAt(i).widget().setStyleSheet('QPushButton { font-family: ' + self.options.getQuizFont() + '; font-size: 11pt; }') else: self.layout_horizontal.itemAt(i).widget().setStyleSheet('QPushButton { font-family: ' + self.options.getQuizFont() + '; font-size: %spt; }' % self.options.getQuizFontSize()) self.layout_horizontal.itemAt(i).widget().setText(readings[i]) except: log.debug(u'Not enough quiz variants for ' + self.srs.getCurrentItem()) def getReadyPostLayout(self): self.sentence.hide() self.update() self.grid_layout = QGridLayout() self.grid_layout.setSpacing(0) self.labels = [] columns_mod = 0 #font size depending on sentence length if len(self.srs.currentExample.sentence) > SENTENCE_MAX: font = QFont(self.options.getSentenceFont(), MIN_FONT_SIZE); columns_mod = 6 else: font = QFont(self.options.getSentenceFont(), self.options.getSentenceFontSize()) #row, column, rows span, columns span, max columns i = 0; j = 0; r = 1; c = 1; n = COLUMNS_MAX + columns_mod for word in self.srs.parseCurrentExample(): label = QLabel(word) label.setFont(font) label.setAttribute(Qt.WA_Hover, True) label.installEventFilter(self.filter) self.labels.append(label) if len(label.text()) > 1: c = len(label.text()) else: c = 1 #Don't ask, really if j + c > n: i = i + 1; j = 0 self.grid_layout.addWidget(self.labels.pop(), i, j, r, c) #NB: Ehh, pop should remove label from list, shouldn't it? if j <= n: j = j + c else: j = 0; i = i + 1 self.grid_layout.setAlignment(Qt.AlignCenter) self.layout_vertical.insertLayout(1, self.grid_layout) self.update() def hideButtonsQuiz(self): self.var_1st.hide() self.var_2nd.hide() self.var_3rd.hide() self.var_4th.hide() self.answered.clicked.connect(self.hideQuizAndWaitForNext) self.answered.show() def showButtonsQuiz(self): self.var_1st.show() self.var_2nd.show() self.var_3rd.show() self.var_4th.show() self.answered.hide() self.answered.disconnect() #################################### # Timers and animations # #################################### def waitUntilNextTimeslot(self): #if self.nextQuizTimer.isActive(): self.nextQuizTimer.stop() self.nextQuizTimer.start(self.options.getRepetitionInterval() * 60 * 1000) #options are in minutes NB: how do neatly I convert minutes to ms? self.startUpdatingTrayTooltip() def beginCountdown(self): self.trayIcon.setToolTip('Quiz in progress!') self.pauseAction.setText('&Pause') # self.pauseAction.setShortcut('P') self.countdownTimer.start(self.options.getCountdownInterval() * 1000) self.progressTimer = RepeatTimer(0.01, self.updateCountdownBar, self.options.getCountdownInterval() * 100) self.progressTimer.start() def updateCountdownBar(self): self.countdown.setValue(self.countdown.value() - 1) #print self.countdown.value() self.countdown.update() #NB: without .update() recursive repaint crushes qt def fade(self): if self.windowOpacity() == 1: self.animationTimer = RepeatTimer(0.025, self.fadeOut, 40) self.animationTimer.start() else: self.animationTimer = RepeatTimer(0.025, self.fadeIn, 40) self.animationTimer.start() def fadeIn(self): self.setWindowOpacity(self.windowOpacity() + 0.1) def fadeOut(self): self.setWindowOpacity(self.windowOpacity() - 0.1) def stopCountdown(self): self.progressTimer.cancel() self.countdownTimer.stop() self.countdown.setValue(0) #################################### # Actions and events # #################################### def setMenus(self): self.showQuizAction = QAction('&Quiz me now!', self, triggered=self.showQuiz) self.showQuizAction.setIcon(QIcon(PATH_TO_RES + TRAY + NOW_ICON)) self.trayMenu.addAction(self.showQuizAction) self.pauseAction = QAction('&Start quiz!', self, triggered=self.pauseQuiz) self.pauseAction.setIcon(QIcon(PATH_TO_RES + TRAY + START_ICON)) self.trayMenu.addAction(self.pauseAction) self.trayMenu.addSeparator() self.quickDictAction = QAction('Quick &dictionary', self, triggered=self.showQuickDict) self.quickDictAction.setIcon(QIcon(PATH_TO_RES + TRAY + DICT_ICON)) self.trayMenu.addAction(self.quickDictAction) self.optionsAction = QAction('&Options', self, triggered=self.showOptions) self.optionsAction.setIcon(QIcon(PATH_TO_RES + TRAY + OPTIONS_ICON)) self.trayMenu.addAction(self.optionsAction) self.quickLoadAction = QAction('Quick &load', self, triggered=self.showQuickLoad) self.quickLoadAction.setIcon(QIcon(PATH_TO_RES + TRAY + LOAD_ICON)) self.trayMenu.addAction(self.quickLoadAction) self.trayMenu.addSeparator() self.aboutAction = QAction('&About', self, triggered=self.showAbout) self.aboutAction.setIcon(QIcon(PATH_TO_RES + TRAY + ABOUT_ICON)) self.trayMenu.addAction(self.aboutAction) self.globalStatsAction = QAction('&Global statistics', self, triggered=self.showGlobalStatistics) self.globalStatsAction.setIcon(QIcon(PATH_TO_RES + TRAY + STAT_ICON)) self.trayMenu.addAction(self.globalStatsAction) self.utilAction = QAction('U&tilities', self, triggered=self.showToolsDialog) self.utilAction.setIcon(QIcon(PATH_TO_RES + TRAY + UTILS_ICON)) self.trayMenu.addAction(self.utilAction) self.trayMenu.addSeparator() self.quitAction = QAction('&Exit', self, triggered=self.saveAndExit) self.quitAction.setIcon(QIcon(PATH_TO_RES + TRAY + CLOSE_ICON)) self.trayMenu.addAction(self.quitAction) self.trayIcon.setContextMenu(self.trayMenu) self.trayIcon.activated.connect(self.onTrayIconActivated) def onTrayIconActivated(self, reason): ''' if reason == QSystemTrayIcon.DoubleClick: print 'tray icon double clicked' ''' if reason == QSystemTrayIcon.Trigger: if self.isHidden(): self.trayIcon.showMessage('Current session statistics:', 'Running time:\t\t' + self.stats.getRunningTime() + '\nItems seen:\t\t' + str(self.stats.totalItemSeen) + '\nCorrect answers:\t\t' + str(self.stats.answeredCorrect) + '\nWrong answers:\t\t' + self.stats.getIncorrectAnswersCount() + '\nCorrect ratio:\t\t' + self.stats.getCorrectRatioPercent() + #'\nQuiz total time:\t\t' + self.stats.getQuizActive() + '\nQuiz paused time:\t\t' + self.stats.getPausedTime() + '\nTotal pondering time:\t' + self.stats.getMusingsTime() + '\nTotal post-quiz time:\t' + self.stats.getQuizTime() + '\nAverage pondering:\t' + self.stats.getAverageMusingTime() + '\nAverage post-quiz:\t' + self.stats.getAveragePostQuizTime(), QSystemTrayIcon.MessageIcon.Information, 20000) def setButtonsActions(self): if self.var_1st.text() == self.srs.getCorrectAnswer(): self.var_1st.clicked.connect(self.correctAnswer) else: self.var_1st.clicked.connect(self.wrongAnswer) if self.var_2nd.text() == self.srs.getCorrectAnswer(): self.var_2nd.clicked.connect(self.correctAnswer) else: self.var_2nd.clicked.connect(self.wrongAnswer) if self.var_3rd.text() == self.srs.getCorrectAnswer(): self.var_3rd.clicked.connect(self.correctAnswer) else: self.var_3rd.clicked.connect(self.wrongAnswer) if self.var_4th.text() == self.srs.getCorrectAnswer(): self.var_4th.clicked.connect(self.correctAnswer) else: self.var_4th.clicked.connect(self.wrongAnswer) self.var_1st.setShortcut('1') self.var_2nd.setShortcut('2') self.var_3rd.setShortcut('3') self.var_4th.setShortcut('4') def resetButtonsActions(self): self.var_1st.disconnect() self.var_2nd.disconnect() self.var_3rd.disconnect() self.var_4th.disconnect() def postAnswerActions(self): self.stats.musingsStopped() self.stats.postQuizStarted() self.stopCountdown() self.hideButtonsQuiz() self.getReadyPostLayout() def refocusQuiz(self): self.answered.setShortcut('Space') self.activateWindow() self.answered.setFocus() def correctAnswer(self): self.postAnswerActions() self.srs.answeredCorrect() self.stats.quizAnsweredCorrect() self.checkTranslationSize(self.srs.getCurrentSentenceTranslation()) self.status.achievements.correctAnswer() self.showSessionMessage(u'<font color=green>Correct: ' + self.srs.getCorrectAnswer() + '</font>\t|\tNext quiz: ' + self.srs.getNextQuizTime() + '\t|\t<font color=' + self.srs.getLeitnerGradeAndColor()['color'] + '>Grade: ' + self.srs.getLeitnerGradeAndColor()['grade'] + ' (' + self.srs.getLeitnerGradeAndColor()['name'] + ')<font>') self.refocusQuiz() def wrongAnswer(self): self.postAnswerActions() self.srs.answeredWrong() self.stats.quizAnsweredWrong() self.checkTranslationSize(self.srs.getCurrentSentenceTranslation()) self.status.achievements.wrongAnswer() self.showSessionMessage(u'<font color=tomato>Wrong! Should be: '+ self.srs.getCorrectAnswer() + '</font>\t|\tNext quiz: ' + self.srs.getNextQuizTime() + '\t|\t<font color=' + self.srs.getLeitnerGradeAndColor()['color'] + '>Grade: ' + self.srs.getLeitnerGradeAndColor()['grade'] + ' (' + self.srs.getLeitnerGradeAndColor()['name'] + ')<font>') self.refocusQuiz() def timeIsOut(self): self.stats.musingsStopped() self.stats.postQuizStarted() QTimer.singleShot(50, self.hideButtonsQuiz) #NB: slight artificial lag to prevent recursive repaint crush (when mouse is suddenly over repainted button) self.getReadyPostLayout() self.srs.answeredWrong() self.stats.quizAnsweredWrong() self.checkTranslationSize(self.srs.getCurrentSentenceTranslation()) self.status.achievements.wrongAnswer() self.showSessionMessage(u'<font color=tomato>Timeout! Should be: ' + self.srs.getCorrectAnswer() + '</font>\t|\tNext quiz: ' + self.srs.getNextQuizTime() + '\t|\t<font color=' + self.srs.getLeitnerGradeAndColor()['color'] + '>Grade: ' + self.srs.getLeitnerGradeAndColor()['grade'] + ' (' + self.srs.getLeitnerGradeAndColor()['name'] + ')<font>') self.refocusQuiz() def checkTranslationSize(self, translation): if len(translation) > TRANSLATION_CHARS_LIMIT: self.answered.setStyleSheet('QPushButton { font-size: 9pt; }') space_indices = [i for i, value in enumerate(translation) if value == ' '] find_nearest_index = lambda value,list : min(list, key = lambda x:abs(x - value)) nearest_index = find_nearest_index(TRANSLATION_CHARS_LIMIT, space_indices) translation = translation[:nearest_index] + '\n' + translation[nearest_index + 1:] else: self.answered.setStyleSheet('QPushButton { font-size: 11pt; }') self.answered.setText(translation) def hideQuizAndWaitForNext(self): self.stats.postQuizEnded() self.status.hide() self.info.hide() self.allInfo.hide() self.resetButtonsActions() self.setWindowOpacity(1) self.fade() QTimer.singleShot(1000, self.hide) self.waitUntilNextTimeslot() self.updater.mayUpdate = True def pauseQuiz(self): if self.isHidden(): if self.pauseAction.text() == '&Pause': self.nextQuizTimer.stop() self.pauseAction.setText('&Unpause') # self.pauseAction.setShortcut('U') self.trayIcon.setToolTip('Quiz paused!') self.trayIcon.setIcon(QIcon(PATH_TO_RES + TRAY + 'inactive.png')) self.trayUpdater.cancel() self.pauseAction.setIcon(QIcon(PATH_TO_RES + TRAY + START_ICON)) self.stats.pauseStarted() self.updater.mayUpdate = True elif self.pauseAction.text() == '&Start quiz!': self.waitUntilNextTimeslot() self.pauseAction.setText('&Pause') # self.pauseAction.setShortcut('P') self.trayIcon.setToolTip('Quiz in progress!') self.trayIcon.setIcon(QIcon(PATH_TO_RES + TRAY + 'active.png')) self.pauseAction.setIcon(QIcon(PATH_TO_RES + TRAY + PAUSE_ICON)) else: self.waitUntilNextTimeslot() self.pauseAction.setText('&Pause') # self.pauseAction.setShortcut('P') self.trayIcon.setToolTip('Quiz in progress!') self.trayIcon.setIcon(QIcon(PATH_TO_RES + TRAY + 'active.png')) self.pauseAction.setIcon(QIcon(PATH_TO_RES + TRAY + PAUSE_ICON)) self.stats.pauseEnded() self.updater.mayUpdate = False else: self.showSessionMessage(u'Sorry, cannot pause while quiz in progress!') def showQuiz(self): if self.isHidden(): self.updateContent() self.setButtonsActions() #self.restoreGeometry(self.gem) self.trayIcon.setIcon(QIcon(PATH_TO_RES + TRAY + 'active.png')) self.show() self.setWindowOpacity(0) self.fade() self.countdown.setValue(self.options.getCountdownInterval() * 100) self.beginCountdown() self.stats.musingsStarted() if self.nextQuizTimer.isActive(): self.nextQuizTimer.stop() self.updater.mayUpdate = False else: self.showSessionMessage(u'Quiz is already underway!') def showOptions(self): self.optionsDialog.show() def showAbout(self): self.about.show() def showQuickDict(self): self.qdict.showQDict = True def showGlobalStatistics(self): self.statistics.show() def showToolsDialog(self): self.tools.show() def showQuickLoad(self): self.qload.show() def startTrayLoading(self): self.gifLoading.start() #self.iconTimer = QTimer() #self.iconTimer.timeout.connect(self.updateTrayIcon) #self.iconTimer.start(100) def stopTrayLoading(self): self.gifLoading.stop() def updateTrayIcon(self): self.trayIcon.setIcon(self.gifLoading.currentPixmap()) def showSessionMessage(self, message): """Shows info message""" self.status.message.setText(message) if self.status.achievements.achieved is not None: self.status.info.setText(self.status.achievements.achieved[1] + '\t( ' + self.status.achievements.achieved[0] + ' )') self.status.progress.hide() self.status.move(self.status.x(), self.status.y() - 15) self.status.info.show() # self.status.setMask(roundCorners(self.status.rect(),5)) else: self.status.info.setText(u'') self.status.info.hide() self.status.restoreGeometry(self.status.gem) # self.status.setMask(roundCorners(self.status.rect(),5)) # print self.status.y() self.status.adjustSize() self.status.show() def saveAndExit(self): self.hide() self.status.hide() self.allInfo.hide() self.kanjiInfo.hide() self.trayIcon.showMessage('Shutting down...', 'Saving session', QSystemTrayIcon.MessageIcon.Information, 20000 ) if self.countdownTimer.isActive(): self.countdownTimer.stop() if self.nextQuizTimer.isActive(): self.nextQuizTimer.stop() if self.progressTimer != () and self.progressTimer.isAlive(): self.progressTimer.cancel() if self.trayUpdater is not None and self.trayUpdater.isAlive() : self.trayUpdater.cancel() self.rehash.checkSessionResults() self.srs.endCurrentSession(self.stats) self.trayIcon.hide() self.hooker.stop() self.updater.stop() self.optionsDialog.close() self.about.close() self.qdict.close() self.close() def addReferences(self, about, options, qdict, updater, tools, statistics, web, rehash): self.about = about self.optionsDialog = options self.qdict = qdict self.updater = updater self.tools = tools self.statistics = statistics self.status.web = web self.rehash = rehash def initGlobalHotkeys(self): def toggleWidgetFlag(): self.qdict.showQDict = True self.hooker = GlobalHotkeyManager(toggleWidgetFlag , 'Q') self.hooker.setDaemon(True) self.hooker.start() def showEvent(self, event): self.restoreGeometry(self.gem)
class NanoKontrol2InputHandler(InputHandler): def __init__(self, *args, **kwargs): InputHandler.__init__(self, *args, **kwargs) self._leds = dict() self._hold_button = None self._hold_timer = None # a port to send messages back to transport controllers self._send_port = None # the names of controller clients that have been connected # to the transport self._connected_controllers = set() self.transport.add_observer(self.on_transport_change) self.on_transport_change() def destroy(self): if (self._send_port is not None): client = self._send_port.client sinks = self._send_port.get_connections() for sink in sinks: client.disconnect(self._send_port, sink) self.transport.remove_observer(self.on_transport_change) InputHandler.destroy(self) # connect back to all controlling devices def on_transport_change(self): # update connections to controlling devices ports = self.port.get_connections() for port in ports: name = port.name.split(':')[0] if (name in self._connected_controllers): continue self.connect_controller(name) # update LED state on controlling devices if (self.transport): self.update_led(self.PLAY_BUTTON, self.transport.playing) self.update_led(self.RECORD_BUTTON, self.transport.recording) self.update_led(self.CYCLE_BUTTON, self.transport.cycling) def connect_controller(self, name): # get an input port for the device client = self.port.client input_ports = client.get_ports(name_pattern=name+':.*', type_pattern='.*midi.*', flags=jackpatch.JackPortIsInput) if (len(input_ports) == 0): return input_port = input_ports[0] if (self._send_port is None): if (self._response_port is not None): self._send_port = self._response_port else: self._send_port = jackpatch.Port(client=client, name="midi.TX", flags=jackpatch.JackPortIsOutput) self.port.client.connect(self._send_port, input_port) self._connected_controllers.add(name) self.init_controllers() def init_controllers(self): # send a sysex message to let the controller know we'll be # managing the state of its LEDs self.send_message([ 0xF0, 0x42, 0x40, 0x00, 0x01, 0x13, 0x00, 0x00, 0x00, 0x01, 0xF7 ]) # turn off all LEDs initially for i in range(0, 128): self.update_led(i, False) # send a message back to the controlling device def send_message(self, data): if (self._send_port is None): return self._send_port.send(data, 0.0) # controller button values PLAY_BUTTON = 0x29 STOP_BUTTON = 0x2A BACK_BUTTON = 0x2B FORWARD_BUTTON = 0x2C RECORD_BUTTON = 0x2D CYCLE_BUTTON = 0x2E PREVIOUS_TRACK_BUTTON = 0x3A NEXT_TRACK_BUTTON = 0x3B SET_MARK_BUTTON = 0x3C PREVIOUS_MARK_BUTTON = 0x3D NEXT_MARK_BUTTON = 0x3E # controller type masks TRANSPORT = 0x28 MARK = 0x38 LEVEL = 0x00 PAN = 0x10 SOLO = 0x20 MUTE = 0x30 ARM = 0x40 PER_TRACK = (LEVEL, PAN, SOLO, MUTE, ARM) # interpret messages def handle_message(self, data, time): # ignore all long messages if (len(data) != 3): return (status, controller, value) = data # filter for control-change messages if ((status & 0xF0) != 0xB0): return # get the kind of control this is kind = (controller & 0xF8) # handle mode buttons if (controller == self.PLAY_BUTTON): if ((value > 64) and (self.transport)): self.transport.play() elif (controller == self.RECORD_BUTTON): if ((value > 64) and (self.transport)): self.transport.record() elif (controller == self.CYCLE_BUTTON): if ((value > 64) and (self.transport)): self.transport.cycling = not self.transport.cycling # handle action buttons elif ((kind == self.TRANSPORT) or (kind == self.MARK)): if (value > 64): if (self.transport): if (controller == self.STOP_BUTTON): self.transport.stop() elif (controller == self.BACK_BUTTON): self.transport.skip_back() self.set_holding(controller) elif (controller == self.FORWARD_BUTTON): self.transport.skip_forward() self.set_holding(controller) elif (controller == self.PREVIOUS_MARK_BUTTON): self.transport.previous_mark() self.set_holding(controller) elif (controller == self.NEXT_MARK_BUTTON): self.transport.next_mark() self.set_holding(controller) elif (controller == self.SET_MARK_BUTTON): self.transport.toggle_mark() self.set_holding(controller) else: self.clear_holding() self.update_led(controller, value > 64) # handle a button being held down def set_holding(self, button): self._hold_button = button if (self._hold_timer is None): self._hold_timer = QTimer(self) self._hold_timer.timeout.connect(self.on_hold) self._hold_timer.setInterval(500) self._hold_timer.start() def clear_holding(self): self._hold_button = None if (self._hold_timer is not None): self._hold_timer.stop() def on_hold(self): if (self._hold_button is None): try: self._hold_timer.stop() except AttributeError: pass return self.handle_message([ 0xBF, self._hold_button, 127 ], 0.0) if ((self._hold_timer is not None) and (self._hold_timer.interval() != 100)): self._hold_timer.setInterval(100) # send a message to update the state of a button LED def update_led(self, button, on): try: old_value = self._leds[button] except KeyError: old_value = None value = 0 if (on): value = 127 if (value != old_value): self._leds[button] = value self.send_message([ 0xBF, button, value ])
class Navigator(Singleton,object): instance = None logger = None PATH_SEPARATOR = '-' JUMP_SIGNAL_PAGE_LOAD_FINISH = 'PAGE LOAD FINISH' JUMP_SIGNAL_REQUEST_LOAD_FINISH = 'REQUEST LOAD FINISH' CROSS_AXIS_METHOD_AJAX = 'AJAX' CROSS_AXIS_METHOD_FULL_LOAD = 'FULL LOAD' def __init__(self): self.network_manager = CustomNetworkAccessManager.getInstance() self.axis = [] self.human_events = {} self.node_collected_data = {} self.last_url = None self.current_url = QUrl() self.current_axis_id = None self.crossed_axis_method = None self.crossed_axes = [] self.crossed_axes_string = '' self.cross_axis_delay = QTimer() self.cross_axis_delay.setSingleShot(True) self.cross_axis_delay.timeout.connect(self.crossedAxis) self.cross_axis_params = None self.loading_timeout = QTimer() self.loading_timeout.setSingleShot(True) self.loading_timeout.setInterval(30000) self.loading_timeout.timeout.connect(self.loadingTimeoutAction) self.inactivity_timeout = QTimer() self.inactivity_timeout.setSingleShot(True) self.inactivity_timeout.setInterval(10000) self.inactivity_timeout.timeout.connect(self.inactivityTimeoutAction) self.page_reloads = 0 Logger.getLoggerFor(self.__class__) def takeEntryPoint(self): g = GraphParser.getInstance() for ax in self.axis: self.logger.info('Axis : %s' % ax) if g.entry_point: self.logger.info('Taking the entry point %s' % g.entry_point) self.last_url = QUrl(g.entry_point) inst = Jaime.getInstance() inst.view.load(QUrl(g.entry_point)) return True self.logger.error("Can't take entry point, graph has't entry point") return False def matchesSelector(self,axis): inst = Jaime.getInstance() frame = inst.view.page().mainFrame() document = frame.documentElement() selector = axis.css_selector_condition # print 'document %s ' % document.evaluateJavaScript("""this.querySelector('%s')""" % selector) ret = frame.findAllElements(selector) print '%s axis %s matches css selector ? %s' % (selector,axis,bool(ret.count())) self.logger.debug('axis %s matches css selector ? %s' % (axis,bool(ret.count()))) # for i in range(ret.count()): # self.logger.error(ret.at(i).toInnerXml()) return ret.count() def matches(self,axis): m_from = False m_to = False m_method = True m_selector = True m_route = True if axis.from_url is not None : if isinstance(axis.from_url,str): m_from = axis.from_url == self.last_url.toString() elif axis.from_url.match(self.last_url.toString()): m_from = True else: m_from = True if axis.to_url is not None: if isinstance(axis.to_url,str): m_to = axis.to_url == self.current_url.toString() elif axis.to_url.match(self.current_url.toString()): m_to = True else: m_to = True # print 'el axis %s tiene method %s, %s ' % (axis, # axis.axis_method, # self.crossed_axis_method # ) if self.crossed_axis_method == RouteAxis.CROSS_AXIS_METHOD_AJAX or \ axis.axis_method : if self.crossed_axis_method != axis.axis_method: m_method = False if axis.css_selector_condition: m_selector = axis.not_css_selector ^ bool(self.matchesSelector(axis)) if len(axis.previous_axis)>0: m_route = False for path_index in range(len(axis.previous_axis)): path = string.join( axis.previous_axis[path_index], self.PATH_SEPARATOR) self.logger.debug( "matching axis (%s) with path %s" % (axis.id,path)) if self.crossed_axes_string.find(path) == 0: m_route = True break self.logger.debug("Matches (%s) %s\nfrom %s to %s method %s selector %s route %s" % (axis.id, axis.comment, m_from,m_to,m_method,m_selector,m_route)) return m_from and m_to and m_method and m_selector and m_route def crossedAxis(self,ajax=False): if self.cross_axis_params and self.cross_axis_params[0]: ajax = True self.cross_axis_params = None self.crossed_axes_string = string.join(self.crossed_axes,self.PATH_SEPARATOR) self.logger.info("crossedAxis, path: \n%s" % string.join(self.crossed_axes,"\n")) if ajax: self.crossed_axis_method = RouteAxis.CROSS_AXIS_METHOD_AJAX else: self.crossed_axis_method = RouteAxis.CROSS_AXIS_METHOD_FULL_LOAD print 'Cruse un eje por %s ' % self.crossed_axis_method for ax in self.axis: if self.matches(ax): self.current_axis_id = ax.id # print 'current axis %s' % ax.id self.logger.info('Axis matched %s' % ax) self.startSecuence(ax) return self.logger.warning("Can't match any axis") self.inactivity_timeout.start() def collectData(self,id,collector,params): if not self.getAxis(id): self.logger.error("Can't collect data from inexistent axis") return if id not in self.node_collected_data: self.node_collected_data[id] = [] self.node_collected_data[id].append((collector,params)) def setAxis(self,route_axis,human_events): self.logger.info('setting axis %s ' % route_axis) if route_axis.id in self.human_events: raise Exception('Axis repetido') self.human_events[route_axis.id] = human_events self.axis.append(route_axis) # QtCore.QObject.disconnect(self.human_events[route_axis.id], # QtCore.SIGNAL("finished()"), # self.secuenceFinished) QtCore.QObject.connect(self.human_events[route_axis.id], QtCore.SIGNAL("finished()"), self.secuenceFinished) def startSecuence(self,axis): inst = Jaime.getInstance() try : if axis.max_crosses > 0 and axis.max_crosses <= axis.cross_counter: self.logger.info('el axis supero la cantidad maxima de loops ') if axis.exit_point: exit_axis = self.getAxis(axis.exit_point) self.logger.info('Salto hacia el exit_axis %s' % exit_axis) self.startSecuence(exit_axis) else: self.logger.error('No hay exit axis muero %s' % axis.exit_point) inst = Jaime.getInstance() inst.finishWork() else: axis.cross_counter += 1 h_ev = self.human_events[axis.id] self.crossed_axes.insert(0,axis.id) if len(self.crossed_axes) >= 10: self.crossed_axes.pop() self.logger.info('Stopeo el inactivity timeout') self.inactivity_timeout.stop() if axis.id in self.node_collected_data: inst.route_node.doWork(self.node_collected_data[axis.id]) h_ev.fire() except Exception as e: # print 'Excepcion en startSecuence %s' % e self.logger.error(e) def getAxis(self,axis_id): for ax in self.axis: if ax.id == axis_id: return ax return None def secuenceFinished(self): self.logger.info('estarteo el inactivity timeout' ) self.inactivity_timeout.start() def inactivityTimeoutAction(self): inst = Jaime.getInstance() self.logger.info('inactivity timeout action fired after %s seconds' % (self.inactivity_timeout.interval()/1000)) if not len(self.crossed_axes): return last_axis = self.crossed_axes[0] ax = self.getAxis(last_axis) retry_axis_id = ax.retry_axis retry_axis = self.getAxis(retry_axis_id) if retry_axis: self.logger.info('El axis %s tiene como retry axis a %s lo estarteo' % (ax.id,ax.retry_axis)) self.startSecuence(retry_axis) def processLoadStarted(self): inst = Jaime.getInstance() self.logger.info('Page started load to %s' % inst.view.url().toString()) # print inst.page.mainFrame().requestedUrl() self.inactivity_timeout.stop() self.loading_timeout.stop() self.loading_timeout.start() self.logger.info('Starting loading timeout and stopping inactivity timeout') self.last_url = inst.view.url() def loadingTimeoutAction(self): self.logger.warning('Loading timeout fired') inst = Jaime.getInstance() if (not inst.view.url().isEmpty() and re.match('http.?://',inst.view.url().toString()) ) \ and not self.page_reloads: self.logger.info('Timeout fired, reloading last url %s' % inst.view.url().toString()) self.page_reloads += 1 self.loading_timeout.stop() inst.view.reload() else: self.logger.error("""Timeout fired, clossing jaime, there isn't last_url or max_reloads reatched""" ) inst.finishWork() def processPageLoadFinished(self,status): inst = Jaime.getInstance() self.logger.info('Page finished load to %s with status %s ' % (inst.view.url().toString(), status)) if status: self.current_url = inst.view.url() self.loading_timeout.stop() self.page_reloads = 0 self.logger.info('Stopping loading timeout') else: self.current_url = QUrl() self.testJumpRules(self.JUMP_SIGNAL_PAGE_LOAD_FINISH, status) def testJumpRules(self,signal,*args): # self.logger.info('Call to restJumpRules with signal %s' % signal) # print 'Call to restJumpRules %s' % ( signal) if signal == self.JUMP_SIGNAL_PAGE_LOAD_FINISH: print 'llamo a crosed axis' self.pushCrossedAxis() elif signal == self.JUMP_SIGNAL_REQUEST_LOAD_FINISH: if not self.current_axis_id: return req_headers = args[0] rep_headers = args[1] ax = self.getAxis(self.current_axis_id) # print '%s tiene exit_method %s' % (ax,ax.axis_exit_method) if ax.axis_exit_method and \ ax.axis_exit_method == RouteAxis.CROSS_AXIS_METHOD_AJAX: if 'X-Requested-With' in req_headers: if ax.axis_exit_method_toggled : ax.axis_exit_method = ax.axis_exit_method_toggled ax.axis_exit_method_toggled = None self.pushCrossedAxis(True) def pushCrossedAxis(self,*params): if self.cross_axis_delay.isActive() or \ self.cross_axis_params is not None: self.logger.warning("""Can't push crossAxis call, there is another call in process""") return self.cross_axis_params = params ax = self.getAxis(self.current_axis_id) if ax and ax.delay_node_test: delay = ax.delay_node_test else: delay = None if delay: self.cross_axis_delay.setInterval(delay) self.cross_axis_delay.start() self.logger.info('Delaying %s seconds crossedAxis call ' % int( int(delay) /1000) ) else: self.crossedAxis()
class Transport(observable.Object, unit.Sink): # extra signals recording_will_start = Signal() recording_started = Signal() recording_will_stop = Signal() recording_stopped = Signal() # regular init stuff def __init__(self, time=0.0, duration=0.0, cycling=False, marks=(), protocol='nanokontrol2'): observable.Object.__init__(self) unit.Sink.__init__(self) # set the interval to update at self.update_interval = 0.5 # get a bridge to the JACK transport self._client = jackpatch.Client('jackdaw-transport') self._client.activate() self._transport = jackpatch.Transport(client=self._client) # make a timer to update the transport model when the time changes self._update_timer = QTimer(self) self._update_timer.setInterval(self.update_interval * 1000) self._update_timer.timeout.connect(self.update_timeout) # set up internal state self._recording = False self._cycling = cycling # store all time marks self.marks = observable.List(marks) self._sorting_marks = False self.marks.add_observer(self.on_marks_change) # the start and end times of the cycle region, which will default # to the next and previous marks if not set externally self._cycle_start_time = None self.cycle_start_time = None self._cycle_end_time = None self.cycle_end_time = None # store the duration, which is notional and non-constraining, # and which is meant to be maintained by users of the transport self._duration = duration # store the time self._local_time = None self.time = time self._last_played_to = time self._last_display_update = 0 self._start_time = None self._local_is_rolling = None # the amount to change time by when the skip buttons are pressed self.skip_delta = 1.0 # seconds # make a port and client for transport control self._sink_type = 'midi' self._sink_port = jackpatch.Port(name='midi.RX', client=self._client, flags=jackpatch.JackPortIsInput) self._source_port = jackpatch.Port(client=self._client, name="midi.TX", flags=jackpatch.JackPortIsOutput) self._input_handler = None self._protocol = None self.protocol = protocol # the amount of time to allow between updates of the display self.display_interval = 0.05 # seconds # start updating self._update_timer.start() self.update() # add methods for easy button binding def play(self, *args): self.playing = True def record(self, *args): self.recording = True def stop(self, *args): self.playing = False self.recording = False # the protocol to use to interpret MIDI input @property def protocol(self): return(self._protocol) @protocol.setter def protocol(self, value): if ((value != self._protocol) and (value in self.protocols)): self._protocol = value (name, cls) = self.protocols[value] if (self._input_handler is not None): self._input_handler.destroy() self._input_handler = cls(port=self.sink_port, response_port=self._source_port, transport=self) # return a map from protocols to handler classes @property def protocols(self): return(collections.OrderedDict(( ('realtime', ('System Realtime', SystemRealtimeInputHandler)), ('nanokontrol2', ('Korg NanoKONTROL2', NanoKontrol2InputHandler)) ))) # whether play mode is on @property def playing(self): return((self.is_rolling) and (not self.recording)) @playing.setter def playing(self, value): value = (value == True) if (self.playing != value): self.recording = False if (value): self.start() else: self.pause() self.on_change() # whether record mode is on @property def recording(self): return((self.is_rolling) and (self._recording)) @recording.setter def recording(self, value): value = (value == True) if (self._recording != value): self.playing = False if (value): self.recording_will_start.emit() else: self.recording_will_stop.emit() self._recording = value if (self._recording): self.start() else: self.pause() self.on_change() if (value): self.recording_started.emit() else: self.recording_stopped.emit() # whether the transport is playing or recording @property def is_rolling(self): if (self._local_is_rolling is not None): return(self._local_is_rolling) return(self._transport.is_rolling) # whether cycle mode is on @property def cycling(self): return(self._cycling) @cycling.setter def cycling(self, value): value = (value == True) if (self._cycling != value): self.update_cycle_bounds() self._cycling = value self.on_change() # get the current timepoint of the transport @property def time(self): if (self._local_time is not None): return(self._local_time) return(self._transport.time) @time.setter def time(self, t): # don't allow the time to be set while recording if (self._recording): return # don't let the time be negative t = max(0.0, t) # record the local time setting so we can use it while the transport # updates to the new location self._local_time = t self._transport.time = t self.update_cycle_bounds() self.on_change() @property def duration(self): return(self._duration) @duration.setter def duration(self, value): if (value != self._duration): self._duration = value self.on_change() # start the time moving forward def start(self): self._last_played_to = self.time # establish the cycle region self.update_cycle_bounds() self._local_is_rolling = True self._transport.start() self.update() # stop time moving forward def pause(self): self._local_is_rolling = False self._transport.stop() self.update() def update_timeout(self): is_rolling = self.is_rolling current_time = self.time # clear the locally stored settings because now we're checking # the real transport self._local_is_rolling = None self._local_time = None self.update(is_rolling=is_rolling, current_time=current_time) def update(self, is_rolling=None, current_time=None): if (is_rolling is None): is_rolling = self.is_rolling if (current_time is None): current_time = self.time # update infrequently when not running and frequently when running if (not is_rolling): if (self._update_timer.interval() != 500): self._update_timer.setInterval(500) self.on_change() else: if (self._update_timer.interval() != 50): self._update_timer.setInterval(50) # do cycling if ((self.cycling) and (self._cycle_end_time is not None) and (current_time > self._cycle_end_time)): # only play up to the cycle end time self.on_play_to(self._cycle_end_time) # bounce back to the start, maintaining any interval we went past the end self._last_played_to = self._cycle_start_time current_time = (self._cycle_start_time + (current_time - self._cycle_end_time)) self.time = current_time # play up to the current time self.on_play_to(current_time) # notify for a display update if the minimum interval has passed elapsed = current_time - self._last_display_update if (abs(elapsed) >= self.display_interval): self.on_change() self._last_display_update = current_time # handle the playback of the span after and including self._last_played_to # and up to but not including the given time def on_play_to(self, end_time): # prepare for the next range self._last_played_to = end_time # set the cycle region based on the current time def update_cycle_bounds(self): current_time = self.time if (self.cycle_start_time is not None): self._cycle_start_time = self.cycle_start_time else: mark = self.get_previous_mark(current_time + 0.001) self._cycle_start_time = mark.time if mark is not None else None if (self.cycle_end_time is not None): self._cycle_end_time = self.cycle_end_time else: mark = self.get_next_mark(current_time) self._cycle_end_time = mark.time if mark is not None else None # jump to the beginning or end def go_to_beginning(self): self.time = 0.0 def go_to_end(self): self.time = self.duration # skip forward or back in time def skip_back(self, *args): self.time = self.time - self.skip_delta def skip_forward(self, *args): self.time = self.time + self.skip_delta # toggle a mark at the current time def toggle_mark(self, *args): UndoManager.begin_action(self.marks) t = self.time found = False for mark in set(self.marks): if (mark.time == t): self.marks.remove(mark) found = True if (not found): self.marks.append(Mark(time=t)) UndoManager.end_action() def on_marks_change(self): if (self._sorting_marks): return self._sorting_marks = True self.marks.sort() self.on_change() self._sorting_marks = False # return the time of the next or previous mark relative to a given time def get_previous_mark(self, from_time): for mark in reversed(self.marks): if (mark.time < from_time): return(mark) # if we're back past the first mark, treat the beginning # like a virtual mark return(Mark(time=0.0)) def get_next_mark(self, from_time): for mark in self.marks: if (mark.time > from_time): return(mark) return(None) # move to the next or previous mark def previous_mark(self, *args): mark = self.get_previous_mark(self.time) if (mark is not None): self.time = mark.time def next_mark(self, *args): mark = self.get_next_mark(self.time) if (mark is not None): self.time = mark.time # transport serialization def serialize(self): return({ 'time': self.time, 'duration': self.duration, 'cycling': self.cycling, 'marks': list(self.marks), 'protocol': self.protocol })