class QDoublePushButton(QPushButton): """加入了双击事件的按钮""" doubleClicked = pyqtSignal() clicked = pyqtSignal() def __init__(self, *args, **kwargs): QPushButton.__init__(self, *args, **kwargs) self.timer = QTimer() self.timer.setSingleShot(True) self.timer.timeout.connect(self.clicked.emit) super().clicked.connect(self.checkDoubleClick) def checkDoubleClick(self): if self.timer.isActive(): self.doubleClicked.emit() self.timer.stop() else: self.timer.start(250)
class WinForm(QWidget): def __init__(self, parent=None): super(WinForm, self).__init__(parent) self.setWindowTitle("QTimer demo") self.listFile = QListWidget() self.label = QLabel("显示当前时间") self.startButton = QPushButton("开始") self.endButton = QPushButton("结束") layout = QGridLayout(self) # 初始化定时器 self.timer = QTimer(self) # 显示时间 self.timer.timeout.connect( self.showTime) # timeout 信号连接到特定的槽,当定时器超时,发出 timeout 信号 layout.addWidget(self.label, 0, 0, 1, 2) layout.addWidget(self.startButton, 1, 0) layout.addWidget(self.endButton, 1, 1) self.startButton.clicked.connect(self.start_timer) self.endButton.clicked.connect(self.end_timer) self.setLayout(layout) def showTime(self): # 获取当前系统时间 time = QDateTime.currentDateTime() # 设置时间格式 timeDisplay = time.toString("yyyy-MM-dd hh:mm:ss dddd") self.label.setText(timeDisplay) def start_timer(self): # 设置时间间隔并启动定时器 self.timer.start(1000) # start 内设置时间间隔,启动或重新启动计时器,如果计时器在运行,则重启 self.startButton.setEnabled(False) self.endButton.setEnabled(True) def end_timer(self): self.timer.stop() # 停止计时器 self.startButton.setEnabled(True) self.endButton.setEnabled(False)
class ProgressBar(QProgressBar): def __init__(self, *args, **kwargs): super(ProgressBar, self).__init__(*args, **kwargs) self.setValue(0) if self.minimum() != self.maximum(): self.timer = QTimer(self) self.timer.timeout.connect(self.onTimeout) self.timer.start(100) def start(self): self.timer.start(100) def stop(self): self.timer.stop() def onTimeout(self): if self.value() >= 100: self.timer.stop() self.timer.deleteLater() del self.timer return self.setValue(self.value() + 1)
class ChangeKeyDialog(QDialog): def __init__( self, parent=None, buttons=None, exercises=None, index: int = None, ): super(ChangeKeyDialog, self).__init__(parent) layout = QVBoxLayout(self) self.setLayout(layout) widget = QWidget() keyLayout = QVBoxLayout() widget.setStyleSheet(""" QWidget{ border-radius: 12px; border: 1px solid grey; background-color: #b5b5b5; color: white; font-size: 40px; } """) # widget.setFixedSize(100, 100) self.currentKeyLabel = QLabel('W') keyLayout.addWidget(self.currentKeyLabel) keyLayout.setAlignment(self.currentKeyLabel, Qt.Alignment.AlignCenter) widget.setLayout(keyLayout) label = QLabel("Press a key to swap") emptyKey = QPushButton('Use empty slot') emptyKey.setFocusPolicy(Qt.FocusPolicy.ClickFocus) emptyKey.clicked.connect(self.useEmpty) acceptKey = QPushButton('Accept') acceptKey.clicked.connect(self.accept) acceptKey.setFocusPolicy(Qt.FocusPolicy.ClickFocus) layout.addWidget(label) layout.addWidget(widget) actions = QHBoxLayout() actions.addWidget(emptyKey) actions.addWidget(acceptKey) layout.addLayout(actions) layout.setAlignment(widget, Qt.Alignment.AlignCenter) self.buttons = buttons self.exercises = exercises self.index = index self.monitor = KeyMonitor() self.monitor.start_monitoring() self.currentKey = self.monitor.currentKey self.timer = QTimer() self.timer.timeout.connect(self.onTimeout) self.timer.start() print("Dialog init done!") def accept(self): currentKeyScheme = [e.assigned_key for e in self.exercises] print(self.exercises[self.index].assigned_key) # Check if pressed key is among if self.currentKey in currentKeyScheme: for name, key in SUPPORTED_KEYS.items( ): # for name, age in dictionary.iteritems(): (for Python 2.x) if key == self.currentKey: old_exercise = None old_button = None for exercise, button in zip(self.exercises, self.buttons): if exercise.assigned_key == ( name, key) and button.text() == name: old_exercise = exercise old_button = button # Set new keys and labels if old_exercise is not None and old_button is not None: self.exercises[ self.index].assigned_key, old_exercise.assigned_key = old_exercise.assigned_key, \ self.exercises[ self.index].assigned_key old_label = old_button.text() old_button.setText(self.buttons[self.index].text()) self.buttons[self.index].setText(old_label) print("old key:", old_exercise.assigned_key) print("new key:", self.exercises[self.index].assigned_key) self.timer.stop() self.close() else: self.exercises[self.index].assigned_key = self.currentKey print( "pos:", list(SUPPORTED_KEYS.keys())[list( SUPPORTED_KEYS.values()).index(self.currentKey[1])]) self.buttons[self.index].setText( list(SUPPORTED_KEYS.keys())[list( SUPPORTED_KEYS.values()).index(self.currentKey[1])]) self.timer.stop() self.close() def useEmpty(self): self.currentKey = ("Empty", None) self.exercises[self.index].assigned_key = self.currentKey self.buttons[self.index].setText(self.currentKey[0]) self.timer.stop() self.close() def onTimeout(self): if self.monitor is not None: if self.monitor.currentKey in list(SUPPORTED_KEYS.values()): if type(self.monitor.currentKey) is KeyCode: self.currentKeyLabel.setText(self.monitor.currentKey.char) else: self.currentKeyLabel.setText(str(self.monitor.currentKey)) name = list(SUPPORTED_KEYS.keys())[list( SUPPORTED_KEYS.values()).index(self.monitor.currentKey)] self.currentKey = (name, self.monitor.currentKey) def writeExerciseKeyMap(self): with open(os.getcwd() + MAPPED_KEYS_PATH + self.subject + '.json', 'w') as fp: json.dump(self.exercises, fp) # static method to create the dialog and return (date, time, accepted) @staticmethod def getSwapper(parent=None): dialog = ChangeKeyDialog(parent) result = dialog.exec() return result == QDialog.DialogCode.Accepted
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.setupTrayicon() self.setupVariables() self.setupUi() self.setupConnections() self.show() def setupVariables(self): settings = QSettings() self.workEndTime = QTime( int(settings.value(workHoursKey, 0)), int(settings.value(workMinutesKey, 25)), int(settings.value(workSecondsKey, 0)), ) self.restEndTime = QTime( int(settings.value(restHoursKey, 0)), int(settings.value(restMinutesKey, 5)), int(settings.value(restSecondsKey, 0)), ) self.timeFormat = "hh:mm:ss" self.time = QTime(0, 0, 0, 0) self.workTime = QTime(0, 0, 0, 0) self.restTime = QTime(0, 0, 0, 0) self.totalTime = QTime(0, 0, 0, 0) self.currentMode = Mode.work self.maxRepetitions = -1 self.currentRepetitions = 0 def setupConnections(self): """ Create button connections """ self.startButton.clicked.connect(self.startTimer) self.startButton.clicked.connect( lambda: self.startButton.setDisabled(True)) self.startButton.clicked.connect( lambda: self.pauseButton.setDisabled(False)) self.startButton.clicked.connect( lambda: self.resetButton.setDisabled(False)) self.pauseButton.clicked.connect(self.pauseTimer) self.pauseButton.clicked.connect( lambda: self.startButton.setDisabled(False)) self.pauseButton.clicked.connect( lambda: self.pauseButton.setDisabled(True)) self.pauseButton.clicked.connect( lambda: self.resetButton.setDisabled(False)) self.pauseButton.clicked.connect( lambda: self.startButton.setText("continue")) self.resetButton.clicked.connect(self.resetTimer) self.resetButton.clicked.connect( lambda: self.startButton.setDisabled(False)) self.resetButton.clicked.connect( lambda: self.pauseButton.setDisabled(True)) self.resetButton.clicked.connect( lambda: self.resetButton.setDisabled(True)) self.resetButton.clicked.connect( lambda: self.startButton.setText("start")) self.acceptTaskButton.pressed.connect(self.insertTask) self.deleteTaskButton.pressed.connect(self.deleteTask) """ Create spinbox connections """ self.workHoursSpinBox.valueChanged.connect(self.updateWorkEndTime) self.workMinutesSpinBox.valueChanged.connect(self.updateWorkEndTime) self.workSecondsSpinBox.valueChanged.connect(self.updateWorkEndTime) self.restHoursSpinBox.valueChanged.connect(self.updateRestEndTime) self.restMinutesSpinBox.valueChanged.connect(self.updateRestEndTime) self.restSecondsSpinBox.valueChanged.connect(self.updateRestEndTime) self.repetitionsSpinBox.valueChanged.connect(self.updateMaxRepetitions) """ Create combobox connections """ self.modeComboBox.currentTextChanged.connect(self.updateCurrentMode) """ Create tablewidget connections """ self.tasksTableWidget.cellDoubleClicked.connect( self.markTaskAsFinished) def setupUi(self): self.size_policy = QSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) """ Create tabwidget """ self.tabWidget = QTabWidget() """ Create tab widgets """ timerWidget = self.setupTimerTab() tasksWidget = self.setupTasksTab() statisticsWidget = self.setupStatisticsTab() """ add tab widgets to tabwidget""" self.timerTab = self.tabWidget.addTab(timerWidget, makeIcon("timer"), "Timer") self.tasksTab = self.tabWidget.addTab(tasksWidget, makeIcon("tasks"), "Tasks") self.statisticsTab = self.tabWidget.addTab(statisticsWidget, makeIcon("statistics"), "Statistics") """ Set mainwindows central widget """ self.setCentralWidget(self.tabWidget) def setupTimerTab(self): settings = QSettings() self.timerContainer = QWidget(self) self.timerContainerLayout = QVBoxLayout(self.timerContainer) self.timerContainer.setLayout(self.timerContainerLayout) """ Create work groupbox""" self.workGroupBox = QGroupBox("Work") self.workGroupBoxLayout = QHBoxLayout(self.workGroupBox) self.workGroupBox.setLayout(self.workGroupBoxLayout) self.workHoursSpinBox = QSpinBox( minimum=0, maximum=24, value=int(settings.value(workHoursKey, 0)), suffix="h", sizePolicy=self.size_policy, ) self.workMinutesSpinBox = QSpinBox( minimum=0, maximum=60, value=int(settings.value(workMinutesKey, 25)), suffix="m", sizePolicy=self.size_policy, ) self.workSecondsSpinBox = QSpinBox( minimum=0, maximum=60, value=int(settings.value(workSecondsKey, 0)), suffix="s", sizePolicy=self.size_policy, ) """ Create rest groupbox""" self.restGroupBox = QGroupBox("Rest") self.restGroupBoxLayout = QHBoxLayout(self.restGroupBox) self.restGroupBox.setLayout(self.restGroupBoxLayout) self.restHoursSpinBox = QSpinBox( minimum=0, maximum=24, value=int(settings.value(restHoursKey, 0)), suffix="h", sizePolicy=self.size_policy, ) self.restMinutesSpinBox = QSpinBox( minimum=0, maximum=60, value=int(settings.value(restMinutesKey, 5)), suffix="m", sizePolicy=self.size_policy, ) self.restSecondsSpinBox = QSpinBox( minimum=0, maximum=60, value=int(settings.value(restSecondsKey, 0)), suffix="s", sizePolicy=self.size_policy, ) self.restGroupBoxLayout.addWidget(self.restHoursSpinBox) self.restGroupBoxLayout.addWidget(self.restMinutesSpinBox) self.restGroupBoxLayout.addWidget(self.restSecondsSpinBox) """ Create other groupbox""" self.otherGroupBox = QGroupBox("Other") self.otherGroupBoxLayout = QHBoxLayout(self.otherGroupBox) self.otherGroupBox.setLayout(self.otherGroupBoxLayout) self.repetitionsLabel = QLabel("Repetitions") self.repetitionsSpinBox = QSpinBox( minimum=0, maximum=10000, value=0, sizePolicy=self.size_policy, specialValueText="∞", ) self.modeLabel = QLabel("Mode") self.modeComboBox = QComboBox(sizePolicy=self.size_policy) self.modeComboBox.addItems(["work", "rest"]) self.otherGroupBoxLayout.addWidget(self.repetitionsLabel) self.otherGroupBoxLayout.addWidget(self.repetitionsSpinBox) self.otherGroupBoxLayout.addWidget(self.modeLabel) self.otherGroupBoxLayout.addWidget(self.modeComboBox) """ Create timer groupbox""" self.lcdDisplayGroupBox = QGroupBox("Time") self.lcdDisplayGroupBoxLayout = QHBoxLayout(self.lcdDisplayGroupBox) self.lcdDisplayGroupBox.setLayout(self.lcdDisplayGroupBoxLayout) self.timeDisplay = QLCDNumber(8, sizePolicy=self.size_policy) self.timeDisplay.setFixedHeight(100) self.timeDisplay.display("00:00:00") self.lcdDisplayGroupBoxLayout.addWidget(self.timeDisplay) """ Create pause, start and reset buttons""" self.buttonContainer = QWidget() self.buttonContainerLayout = QHBoxLayout(self.buttonContainer) self.buttonContainer.setLayout(self.buttonContainerLayout) self.startButton = self.makeButton("start", disabled=False) self.resetButton = self.makeButton("reset") self.pauseButton = self.makeButton("pause") """ Add widgets to container """ self.workGroupBoxLayout.addWidget(self.workHoursSpinBox) self.workGroupBoxLayout.addWidget(self.workMinutesSpinBox) self.workGroupBoxLayout.addWidget(self.workSecondsSpinBox) self.timerContainerLayout.addWidget(self.workGroupBox) self.timerContainerLayout.addWidget(self.restGroupBox) self.timerContainerLayout.addWidget(self.otherGroupBox) self.timerContainerLayout.addWidget(self.lcdDisplayGroupBox) self.buttonContainerLayout.addWidget(self.pauseButton) self.buttonContainerLayout.addWidget(self.startButton) self.buttonContainerLayout.addWidget(self.resetButton) self.timerContainerLayout.addWidget(self.buttonContainer) return self.timerContainer def setupTasksTab(self): settings = QSettings() """ Create vertical tasks container """ self.tasksWidget = QWidget(self.tabWidget) self.tasksWidgetLayout = QVBoxLayout(self.tasksWidget) self.tasksWidget.setLayout(self.tasksWidgetLayout) """ Create horizontal input container """ self.inputContainer = QWidget() self.inputContainer.setFixedHeight(50) self.inputContainerLayout = QHBoxLayout(self.inputContainer) self.inputContainerLayout.setContentsMargins(0, 0, 0, 0) self.inputContainer.setLayout(self.inputContainerLayout) """ Create text edit """ self.taskTextEdit = QTextEdit( placeholderText="Describe your task briefly.", undoRedoEnabled=True) """ Create vertical buttons container """ self.inputButtonContainer = QWidget() self.inputButtonContainerLayout = QVBoxLayout( self.inputButtonContainer) self.inputButtonContainerLayout.setContentsMargins(0, 0, 0, 0) self.inputButtonContainer.setLayout(self.inputButtonContainerLayout) """ Create buttons """ self.acceptTaskButton = QToolButton(icon=makeIcon("check")) self.deleteTaskButton = QToolButton(icon=makeIcon("trash")) """ Create tasks tablewidget """ self.tasksTableWidget = QTableWidget(0, 1) self.tasksTableWidget.setHorizontalHeaderLabels(["Tasks"]) self.tasksTableWidget.horizontalHeader().setStretchLastSection(True) self.tasksTableWidget.verticalHeader().setVisible(False) self.tasksTableWidget.setWordWrap(True) self.tasksTableWidget.setTextElideMode(Qt.TextElideMode.ElideNone) self.tasksTableWidget.setEditTriggers( QAbstractItemView.EditTriggers.NoEditTriggers) self.tasksTableWidget.setSelectionMode( QAbstractItemView.SelectionMode.SingleSelection) self.insertTasks(*settings.value(tasksKey, [])) """ Add widgets to container widgets """ self.inputButtonContainerLayout.addWidget(self.acceptTaskButton) self.inputButtonContainerLayout.addWidget(self.deleteTaskButton) self.inputContainerLayout.addWidget(self.taskTextEdit) self.inputContainerLayout.addWidget(self.inputButtonContainer) self.tasksWidgetLayout.addWidget(self.inputContainer) self.tasksWidgetLayout.addWidget(self.tasksTableWidget) return self.tasksWidget def setupStatisticsTab(self): """ Create statistics container """ self.statisticsContainer = QWidget() self.statisticsContainerLayout = QVBoxLayout(self.statisticsContainer) self.statisticsContainer.setLayout(self.statisticsContainerLayout) """ Create work time groupbox """ self.statisticsWorkTimeGroupBox = QGroupBox("Work Time") self.statisticsWorkTimeGroupBoxLayout = QHBoxLayout() self.statisticsWorkTimeGroupBox.setLayout( self.statisticsWorkTimeGroupBoxLayout) self.statisticsWorkTimeDisplay = QLCDNumber(8) self.statisticsWorkTimeDisplay.display("00:00:00") self.statisticsWorkTimeGroupBoxLayout.addWidget( self.statisticsWorkTimeDisplay) """ Create rest time groupbox """ self.statisticsRestTimeGroupBox = QGroupBox("Rest Time") self.statisticsRestTimeGroupBoxLayout = QHBoxLayout() self.statisticsRestTimeGroupBox.setLayout( self.statisticsRestTimeGroupBoxLayout) self.statisticsRestTimeDisplay = QLCDNumber(8) self.statisticsRestTimeDisplay.display("00:00:00") self.statisticsRestTimeGroupBoxLayout.addWidget( self.statisticsRestTimeDisplay) """ Create total time groupbox """ self.statisticsTotalTimeGroupBox = QGroupBox("Total Time") self.statisticsTotalTimeGroupBoxLayout = QHBoxLayout() self.statisticsTotalTimeGroupBox.setLayout( self.statisticsTotalTimeGroupBoxLayout) self.statisticsTotalTimeDisplay = QLCDNumber(8) self.statisticsTotalTimeDisplay.display("00:00:00") self.statisticsTotalTimeGroupBoxLayout.addWidget( self.statisticsTotalTimeDisplay) """ Add widgets to container """ self.statisticsContainerLayout.addWidget( self.statisticsTotalTimeGroupBox) self.statisticsContainerLayout.addWidget( self.statisticsWorkTimeGroupBox) self.statisticsContainerLayout.addWidget( self.statisticsRestTimeGroupBox) return self.statisticsContainer def setupTrayicon(self): self.trayIcon = QSystemTrayIcon(makeIcon("tomato")) self.trayIcon.setContextMenu(QMenu()) self.quitAction = self.trayIcon.contextMenu().addAction( makeIcon("exit"), "Quit", self.exit) self.quitAction.triggered.connect(self.exit) self.trayIcon.activated.connect(self.onActivate) self.trayIcon.show() self.trayIcon.setToolTip("Pomodoro") self.toast = ToastNotifier() def leaveEvent(self, event): super(MainWindow, self).leaveEvent(event) self.tasksTableWidget.clearSelection() def closeEvent(self, event): super(MainWindow, self).closeEvent(event) settings = QSettings() settings.setValue(workHoursKey, self.workHoursSpinBox.value()) settings.setValue( workMinutesKey, self.workMinutesSpinBox.value(), ) settings.setValue( workSecondsKey, self.workSecondsSpinBox.value(), ) settings.setValue(restHoursKey, self.restHoursSpinBox.value()) settings.setValue( restMinutesKey, self.restMinutesSpinBox.value(), ) settings.setValue( restSecondsKey, self.restSecondsSpinBox.value(), ) tasks = [] for i in range(self.tasksTableWidget.rowCount()): item = self.tasksTableWidget.item(i, 0) if not item.font().strikeOut(): tasks.append(item.text()) settings.setValue(tasksKey, tasks) def startTimer(self): try: if not self.timer.isActive(): self.createTimer() except: self.createTimer() def createTimer(self): self.timer = QTimer() self.timer.timeout.connect(self.updateTime) self.timer.timeout.connect(self.maybeChangeMode) self.timer.setInterval(1000) self.timer.setSingleShot(False) self.timer.start() def pauseTimer(self): try: self.timer.stop() self.timer.disconnect() except: pass def resetTimer(self): try: self.pauseTimer() self.time = QTime(0, 0, 0, 0) self.displayTime() except: pass def maybeStartTimer(self): if self.currentRepetitions != self.maxRepetitions: self.startTimer() started = True else: self.currentRepetitions = 0 started = False return started def updateWorkEndTime(self): self.workEndTime = QTime( self.workHoursSpinBox.value(), self.workMinutesSpinBox.value(), self.workSecondsSpinBox.value(), ) def updateRestEndTime(self): self.restEndTime = QTime( self.restHoursSpinBox.value(), self.restMinutesSpinBox.value(), self.restSecondsSpinBox.value(), ) def updateCurrentMode(self, mode: str): self.currentMode = Mode.work if mode == "work" else Mode.rest def updateTime(self): self.time = self.time.addSecs(1) self.totalTime = self.totalTime.addSecs(1) if self.modeComboBox.currentText() == "work": self.workTime = self.workTime.addSecs(1) else: self.restTime = self.restTime.addSecs(1) self.displayTime() def updateMaxRepetitions(self, value): if value == 0: self.currentRepetitions = 0 self.maxRepetitions = -1 else: self.maxRepetitions = 2 * value def maybeChangeMode(self): if self.currentMode is Mode.work and self.time >= self.workEndTime: self.resetTimer() self.modeComboBox.setCurrentIndex(1) self.incrementCurrentRepetitions() started = self.maybeStartTimer() self.showWindowMessage( Status.workFinished if started else Status.repetitionsReached) if not started: self.resetButton.click() elif self.currentMode is Mode.rest and self.time >= self.restEndTime: self.resetTimer() self.modeComboBox.setCurrentIndex(0) self.incrementCurrentRepetitions() started = self.maybeStartTimer() self.showWindowMessage( Status.restFinished if started else Status.repetitionsReached) if not started: self.resetButton.click() def incrementCurrentRepetitions(self): if self.maxRepetitions > 0: self.currentRepetitions += 1 def insertTask(self): task = self.taskTextEdit.toPlainText() self.insertTasks(task) def insertTasks(self, *tasks): for task in tasks: if task: rowCount = self.tasksTableWidget.rowCount() self.tasksTableWidget.setRowCount(rowCount + 1) self.tasksTableWidget.setItem(rowCount, 0, QTableWidgetItem(task)) self.tasksTableWidget.resizeRowsToContents() self.taskTextEdit.clear() def deleteTask(self): selectedIndexes = self.tasksTableWidget.selectedIndexes() if selectedIndexes: self.tasksTableWidget.removeRow(selectedIndexes[0].row()) def markTaskAsFinished(self, row, col): item = self.tasksTableWidget.item(row, col) font = self.tasksTableWidget.item(row, col).font() font.setStrikeOut(False if item.font().strikeOut() else True) item.setFont(font) def displayTime(self): self.timeDisplay.display(self.time.toString(self.timeFormat)) self.statisticsRestTimeDisplay.display( self.restTime.toString(self.timeFormat)) self.statisticsWorkTimeDisplay.display( self.workTime.toString(self.timeFormat)) self.statisticsTotalTimeDisplay.display( self.totalTime.toString(self.timeFormat)) def showWindowMessage(self, status): if status is Status.workFinished: title, text = "Break", choice(work_finished_phrases) elif status is Status.restFinished: title, text = "Work", choice(rest_finished_phrases) else: title, text = "Finished", choice(work_finished_phrases) self.trayIcon.showMessage(title, text, makeIcon("tomato")) self.toast.show_toast(title, text, icon_path="pomodoro/data/icons/tomato.ico", duration=10, threaded=True) def makeButton(self, text, iconName=None, disabled=True): button = QPushButton(text, sizePolicy=self.size_policy) if iconName: button.setIcon(makeIcon(iconName)) button.setDisabled(disabled) return button def exit(self): self.close() app = QApplication.instance() if app: app.quit() def onActivate(self, reason): if reason == QSystemTrayIcon.ActivationReason.Trigger: self.show()
class HttpRequestData(QObject): # Add some tolerance for scheduling the QTimer to check for timeouts, because the QTimer may trigger the event a # little earlier. For example, with a 5000ms interval, the timer event can be triggered after 4752ms, so a request # may never timeout if we don't add some tolerance. # 4752ms (actual) and 5000ms (expected) has about 6% difference, so here I use 15% to be safer. TIMEOUT_CHECK_TOLERANCE = 0.15 def __init__(self, request_id: str, http_method: str, request: "QNetworkRequest", manager_timeout_callback: Callable[["HttpRequestData"], None], data: Optional[Union[bytes, bytearray]] = None, callback: Optional[Callable[["QNetworkReply"], None]] = None, error_callback: Optional[ Callable[["QNetworkReply", "QNetworkReply.NetworkError"], None]] = None, download_progress_callback: Optional[Callable[[int, int], None]] = None, upload_progress_callback: Optional[Callable[[int, int], None]] = None, timeout: Optional[float] = None, reply: Optional["QNetworkReply"] = None, parent: Optional["QObject"] = None) -> None: super().__init__(parent=parent) # Sanity checks if timeout is not None and timeout <= 0: raise ValueError( "Timeout must be a positive value, but got [%s] instead." % timeout) self._request_id = request_id self.http_method = http_method self.request = request self.data = data self.callback = callback self.error_callback = error_callback self.download_progress_callback = download_progress_callback self.upload_progress_callback = upload_progress_callback self._timeout = timeout self.reply = reply # For benchmarking. For calculating the time a request spent pending. self._create_time = time.time() # The timestamp when this request was initially issued to the QNetworkManager. This field to used to track and # manage timeouts (if set) for the requests. self._start_time = None # type: Optional[float] self.is_aborted_due_to_timeout = False self._last_response_time = float(0) self._timeout_timer = QTimer(parent=self) if self._timeout is not None: self._timeout_timer.setSingleShot(True) timeout_check_interval = int(self._timeout * 1000 * (1 + self.TIMEOUT_CHECK_TOLERANCE)) self._timeout_timer.setInterval(timeout_check_interval) self._timeout_timer.timeout.connect(self._onTimeoutTimerTriggered) self._manager_timeout_callback = manager_timeout_callback @property def request_id(self) -> str: return self._request_id @property def timeout(self) -> Optional[float]: return self._timeout @property def start_time(self) -> Optional[float]: return self._start_time # For benchmarking. Time in seconds that this request stayed in the pending queue. @property def pending_time(self) -> Optional[float]: if self._start_time is None: return None return self._start_time - self._create_time # Sets the start time of this request. This is called when this request is issued to the QNetworkManager. def setStartTime(self, start_time: float) -> None: self._start_time = start_time # Prepare timeout handling if self._timeout is not None: self._last_response_time = start_time self._timeout_timer.start() # Do some cleanup, such as stopping the timeout timer. def setDone(self) -> None: if self._timeout is not None: self._timeout_timer.stop() self._timeout_timer.timeout.disconnect( self._onTimeoutTimerTriggered) # Since Qt 5.12, pyqtSignal().connect() will return a Connection instance that represents a connection. This # Connection instance can later be used to disconnect for cleanup purpose. We are using Qt 5.10 and this feature # is not available yet, and I'm not sure if disconnecting a lambda can potentially cause issues. For this reason, # I'm using the following facade callback functions to handle the lambda function cases. def onDownloadProgressCallback(self, bytes_received: int, bytes_total: int) -> None: # Update info for timeout handling if self._timeout is not None: now = time.time() time_last = now - self._last_response_time self._last_response_time = time.time() # We've got a response, restart the timeout timer self._timeout_timer.start() if self.download_progress_callback is not None: self.download_progress_callback(bytes_received, bytes_total) def onUploadProgressCallback(self, bytes_sent: int, bytes_total: int) -> None: # Update info for timeout handling if self._timeout is not None: now = time.time() time_last = now - self._last_response_time self._last_response_time = time.time() # We've got a response, restart the timeout timer self._timeout_timer.start() if self.upload_progress_callback is not None: self.upload_progress_callback(bytes_sent, bytes_total) def _onTimeoutTimerTriggered(self) -> None: # Make typing happy if self._timeout is None: return if self.reply is None: return now = time.time() time_last = now - self._last_response_time if self.reply.isRunning() and time_last >= self._timeout: self._manager_timeout_callback(self) else: self._timeout_timer.start() def __str__(self) -> str: data = "no-data" if self.data: data = str(self.data[:10]) if len(self.data) > 10: data += "..." return "request[{id}][{method}][{url}][timeout={timeout}][{data}]".format( id=self._request_id[:8], method=self.http_method, url=self.request.url(), timeout=self._timeout, data=data)
class Window(QMainWindow): # The following attributes are dynamically loaded from the .ui file startButton: QPushButton stopButton: QPushButton leftEyeThreshold: QSlider rightEyeThreshold: QSlider def __init__(self, video_source: FrameSource, capture: Capture): super(Window, self).__init__() loadUi(settings.GUI_FILE_PATH, self) with open(settings.STYLE_FILE_PATH, "r") as css: self.setStyleSheet(css.read()) self.startButton.clicked.connect(self.start) self.stopButton.clicked.connect(self.stop) self.timer = None self.video_source = video_source self.capture = capture def start(self): self.video_source.start() self.timer = QTimer(self) self.timer.timeout.connect(self.update_frame) self.timer.start(settings.REFRESH_PERIOD) def stop(self): self.timer.stop() self.video_source.stop() def update_frame(self): frame = self.video_source.next_frame() face, l_eye, r_eye = self.capture.process( frame, self.leftEyeThreshold.value(), self.rightEyeThreshold.value()) if face is not None: self.display_image(self.opencv_to_qt(frame)) if l_eye is not None: self.display_image(self.opencv_to_qt(l_eye), window="leftEyeBox") if r_eye is not None: self.display_image(self.opencv_to_qt(r_eye), window="rightEyeBox") @staticmethod def opencv_to_qt(img) -> QImage: """ Convert OpenCV image to PyQT image by changing format to RGB/RGBA from BGR """ qformat = QImage.Format.Format_Indexed8 if len(img.shape) == 3: if img.shape[2] == 4: # RGBA qformat = QImage.Format.Format_RGBA8888 else: # RGB qformat = QImage.Format.Format_RGB888 img = numpy.require(img, numpy.uint8, "C") out_image = QImage(img, img.shape[1], img.shape[0], img.strides[0], qformat) # BGR to RGB out_image = out_image.rgbSwapped() return out_image def display_image(self, img: QImage, window="baseImage"): """ Display the image on a window - which is a label specified in the GUI .ui file """ display_label: QLabel = getattr(self, window, None) if display_label is None: raise ValueError(f"No such display window in GUI: {window}") display_label.setPixmap(QPixmap.fromImage(img)) display_label.setScaledContents(True)
class MPSM2PrintJobOutputModel(PrintJobOutputModel): """Print Job Output Model.""" def __init__(self, output_controller: PrinterOutputController) -> None: """Constructor. Args: output_controller: Printer's output controller. """ super().__init__(output_controller=output_controller, key='', name='') self._state = 'not_started' self._progress = 0 # type: int self._elapsed_print_time_millis = 0 # type: int self._elapsed_percentage_points = None # type: Optional[int] # Estimated printing time left, in seconds. self._remaining_print_time_secs = _MAX_REMAINING_TIME_SECS self._stopwatch = QTimer(self) self._stopwatch.timeout.connect(self._tick) self._reset() @pyqtProperty(int) def progress(self) -> int: """UI label for printing progress. Superclass computes progress based on elapsed time. MPSM2 printers do not provide elapsed time, but progress in percentage. Returns: Print job progress from 0 to 100. """ return self._progress @pyqtProperty(str) def estimated_time_left(self) -> str: """UI label for estimated time left. Returns: Human-readable estimated printing time left. """ if self._elapsed_percentage_points is None: return '' if self._elapsed_percentage_points < _MIN_PERCENT_POINTS: return '' return TimeUtils.get_human_readable_countdown( seconds=self._remaining_print_time_secs) def update_progress(self, progress: int) -> None: """Updates job progress and calculates estimated printing time left. Args: progress: Job progress from 0 to 100. """ if progress < 0 or progress > 100: raise ValueError(f'Invalid printing progress: {progress}.') if progress == 0: self._reset() elif self._progress != progress: if self._elapsed_percentage_points is None: # Skip first seen percent point. self._elapsed_percentage_points = 0 elif self._elapsed_percentage_points == 0: self._elapsed_percentage_points += 1 if not self._stopwatch.isActive(): # New percent point seen. Start measuring. self._stopwatch.start(_POLL_INTERVAL_MILLIS) else: self._remaining_print_time_secs = self._calculate_remaining_print_time( ) self._elapsed_percentage_points += 1 self._progress = progress def _reset(self) -> None: """Resets variables to calculate estimated print time left.""" self._elapsed_print_time_millis = 0 self._elapsed_percentage_points = None self._remaining_print_time_secs = _MAX_REMAINING_TIME_SECS if self._stopwatch.isActive(): self._stopwatch.stop() def _calculate_remaining_print_time(self) -> int: """Calculates remaining print time. Calculation is based on the printing time and progress so far. Returns Remaining print time in seconds. """ return int((100 - self._progress) * self._elapsed_print_time_millis / self._elapsed_percentage_points / 1000) def _tick(self) -> None: """Updates stopwatch.""" self._elapsed_print_time_millis += _POLL_INTERVAL_MILLIS
class PrintJobUploadProgressMessage(Message): """Message displayed when a print upload is in progress.""" MIN_CALCULATION_TIME_MILLIS = 5000 POLL_TIME_MILLIS = 10 MAX_REMAINING_MILLIS = 24 * 60 * 60 * 1000 # arbitrary max CALCULATING_TEXT = I18N_CATALOG.i18nc('@info:status', 'Calculating time left...') def __init__(self, on_cancelled: Callable) -> None: """Constructor. Args: on_cancelled: Called when user cancels printer upload. """ super().__init__(title=I18N_CATALOG.i18nc( '@info:status', 'Uploading model to printer'), text=self.CALCULATING_TEXT, progress=-1, lifetime=0, dismissable=False, use_inactivity_timer=False) self._on_cancelled = on_cancelled self._elapsed_upload_time_millis = 0 self._remaining_time_millis = self.MAX_REMAINING_MILLIS self.addAction('cancel', I18N_CATALOG.i18nc('@action:button', 'Cancel'), 'cancel', I18N_CATALOG.i18nc('@action', 'Cancels job upload.')) self.actionTriggered.connect(self._on_action_triggered) self._stopwatch = QTimer(self) self._stopwatch.timeout.connect(self._tick) self._reset_calculation_time() def show(self) -> None: """See base class.""" self.setProgress(0) super().show() self._stopwatch.start(self.POLL_TIME_MILLIS) self._reset_calculation_time() def hide(self, send_signal=True) -> None: """See base class.""" super().hide() self._stopwatch.stop() self._reset_calculation_time() def update(self, bytes_sent: int, bytes_total: int) -> None: """Updates the progress bar. Args: bytes_sent: Number of bytes sent. bytes_total: Target bytes. """ percentage = (bytes_sent / bytes_total) if bytes_total else 0 self.setProgress(percentage * 100) if self._elapsed_upload_time_millis > self.MIN_CALCULATION_TIME_MILLIS: speed = bytes_sent / self._elapsed_upload_time_millis remaining_millis = (bytes_total - bytes_sent) / speed if speed else 0 # Only go down. if remaining_millis < self._remaining_time_millis: self._remaining_time_millis = remaining_millis self.setText( TimeUtils.get_human_readable_countdown( seconds=int(remaining_millis / 1000))) def _reset_calculation_time(self) -> None: """Resets the estimated calculation time.""" self._elapsed_upload_time_millis = 0 self._remaining_time_millis = self.MAX_REMAINING_MILLIS self.setText(self.CALCULATING_TEXT) def _on_action_triggered(self, message: str, action: str) -> None: """Called when an action from user was triggered. Args: message: Message (ignored). action: Action triggered. """ if action == 'cancel': self._on_cancelled() def _tick(self) -> None: """Updates stopwatch.""" self._elapsed_upload_time_millis += self.POLL_TIME_MILLIS