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 __init__(self, app): self.icon = QIcon( resourcePath(os.path.join("vi", "ui", "res", "logo_small.png")) ) QSystemTrayIcon.__init__(self, self.icon, app) self.setToolTip("Your Spyglass Information Service! :)") self.lastNotifications = {} self.setContextMenu(TrayContextMenu(self)) self.showAlarm = True self.showRequest = True self.alarmDistance = 0
def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.TRAY = QSystemTrayIcon(self) self.TRAY.setIcon(QApplication.style().standardIcon( QStyle.StandardPixmap.SP_MessageBoxInformation)) self.TRAY.setToolTip('FGO-py') self.MENU_TRAY = QMenu(self) self.MENU_TRAY_QUIT = QAction('退出', self.MENU_TRAY) self.MENU_TRAY.addAction(self.MENU_TRAY_QUIT) self.MENU_TRAY_FORCEQUIT = QAction('强制退出', self.MENU_TRAY) self.MENU_TRAY.addAction(self.MENU_TRAY_FORCEQUIT) self.TRAY.setContextMenu(self.MENU_TRAY) self.TRAY.show() self.reloadTeamup() self.config = Config({ 'stopOnDefeated': (self.MENU_SETTINGS_DEFEATED, fgoKernel.schedule.stopOnDefeated), 'stopOnKizunaReisou': (self.MENU_SETTINGS_KIZUNAREISOU, fgoKernel.schedule.stopOnKizunaReisou), 'closeToTray': (self.MENU_CONTROL_TRAY, None), 'stayOnTop': (self.MENU_CONTROL_STAYONTOP, lambda x: self.setWindowFlag( Qt.WindowType.WindowStaysOnTopHint, x)), 'notifyEnable': (self.MENU_CONTROL_NOTIFY, None) }) self.notifier = ServerChann(**self.config['notifyParam']) self.worker = Thread() self.signalFuncBegin.connect(self.funcBegin) self.signalFuncEnd.connect(self.funcEnd) self.TRAY.activated.connect(lambda reason: self.show( ) if reason == QSystemTrayIcon.ActivationReason.Trigger else None) self.MENU_TRAY_QUIT.triggered.connect(lambda: QApplication.quit() if self.askQuit() else None) self.MENU_TRAY_FORCEQUIT.triggered.connect(QApplication.quit) self.getDevice()
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 MyMainWindow(QMainWindow, Ui_fgoMainWindow): signalFuncBegin = pyqtSignal() signalFuncEnd = pyqtSignal(object) def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) self.TRAY = QSystemTrayIcon(self) self.TRAY.setIcon(QApplication.style().standardIcon( QStyle.StandardPixmap.SP_MessageBoxInformation)) self.TRAY.setToolTip('FGO-py') self.MENU_TRAY = QMenu(self) self.MENU_TRAY_QUIT = QAction('退出', self.MENU_TRAY) self.MENU_TRAY.addAction(self.MENU_TRAY_QUIT) self.MENU_TRAY_FORCEQUIT = QAction('强制退出', self.MENU_TRAY) self.MENU_TRAY.addAction(self.MENU_TRAY_FORCEQUIT) self.TRAY.setContextMenu(self.MENU_TRAY) self.TRAY.show() self.reloadTeamup() self.config = Config({ 'stopOnDefeated': (self.MENU_SETTINGS_DEFEATED, fgoKernel.schedule.stopOnDefeated), 'stopOnKizunaReisou': (self.MENU_SETTINGS_KIZUNAREISOU, fgoKernel.schedule.stopOnKizunaReisou), 'closeToTray': (self.MENU_CONTROL_TRAY, None), 'stayOnTop': (self.MENU_CONTROL_STAYONTOP, lambda x: self.setWindowFlag( Qt.WindowType.WindowStaysOnTopHint, x)), 'notifyEnable': (self.MENU_CONTROL_NOTIFY, None) }) self.notifier = ServerChann(**self.config['notifyParam']) self.worker = Thread() self.signalFuncBegin.connect(self.funcBegin) self.signalFuncEnd.connect(self.funcEnd) self.TRAY.activated.connect(lambda reason: self.show( ) if reason == QSystemTrayIcon.ActivationReason.Trigger else None) self.MENU_TRAY_QUIT.triggered.connect(lambda: QApplication.quit() if self.askQuit() else None) self.MENU_TRAY_FORCEQUIT.triggered.connect(QApplication.quit) self.getDevice() def keyPressEvent(self, key): if self.MENU_CONTROL_MAPKEY.isChecked( ) and not key.modifiers() & ~Qt.KeyboardModifier.KeypadModifier: try: fgoKernel.device.press(chr(key.nativeVirtualKey())) except KeyError: pass except Exception as e: logger.critical(e) def closeEvent(self, event): if self.config['closeToTray']: self.hide() return event.ignore() if self.askQuit(): return event.accept() event.ignore() def askQuit(self): if self.worker.is_alive(): if QMessageBox.warning( self, 'FGO-py', '战斗正在进行,确认关闭?', QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox. StandardButton.No) != QMessageBox.StandardButton.Yes: return False fgoKernel.schedule.stop('Quit') self.worker.join() self.TRAY.hide() self.config.save() return True def isDeviceAvailable(self): if not fgoKernel.device.available: self.LBL_DEVICE.clear() QMessageBox.critical(self, 'FGO-py', '未连接设备') return False return True def runFunc(self, func, *args, **kwargs): if not self.isDeviceAvailable(): return def f(): try: self.signalFuncBegin.emit() self.applyAll() fgoKernel.schedule.reset() fgoKernel.fuse.reset() func(*args, **kwargs) except fgoKernel.ScriptTerminate as e: logger.critical(e) msg = (str(e), QSystemTrayIcon.MessageIcon.Warning) except BaseException as e: logger.exception(e) msg = (repr(e), QSystemTrayIcon.MessageIcon.Critical) else: msg = ('Done', QSystemTrayIcon.MessageIcon.Information) finally: self.signalFuncEnd.emit(msg) if self.config['notifyEnable'] and not self.notifier(msg[0]): logger.critical('Notify post failed') self.worker = Thread( target=f, name= f'{getattr(func,"__qualname__",getattr(type(func),"__qualname__",repr(func)))}({",".join(repr(i)for i in args)}{","if kwargs else""}{",".join("%s=%r"%i for i in kwargs.items())})' ) self.worker.start() def funcBegin(self): self.BTN_ONEBATTLE.setEnabled(False) self.BTN_MAIN.setEnabled(False) self.BTN_USER.setEnabled(False) self.BTN_PAUSE.setEnabled(True) self.BTN_PAUSE.setChecked(False) self.BTN_STOP.setEnabled(True) self.BTN_STOPLATER.setEnabled(True) self.MENU_SCRIPT.setEnabled(False) self.TXT_APPLE.setValue(0) def funcEnd(self, msg): self.BTN_ONEBATTLE.setEnabled(True) self.BTN_MAIN.setEnabled(True) self.BTN_USER.setEnabled(True) self.BTN_PAUSE.setEnabled(False) self.BTN_STOP.setEnabled(False) self.BTN_STOPLATER.setChecked(False) self.BTN_STOPLATER.setEnabled(False) self.MENU_SCRIPT.setEnabled(True) self.TRAY.showMessage('FGO-py', *msg) def loadTeam(self, teamName): self.TXT_TEAM.setValue(int(self.teamup[teamName]['teamIndex'])) (lambda skillInfo: [ getattr(self, f'TXT_SKILL_{i}_{j}_{k}').setValue(skillInfo[i][j][ k]) for i in range(6) for j in range(3) for k in range(4) ])(eval(self.teamup[teamName]['skillInfo'])) (lambda houguInfo: [ getattr(self, f'TXT_HOUGU_{i}_{j}').setValue(houguInfo[i][j]) for i in range(6) for j in range(2) ])(eval(self.teamup[teamName]['houguInfo'])) (lambda masterSkill: [ getattr(self, f'TXT_MASTER_{i}_{j}').setValue(masterSkill[i][j]) for i in range(3) for j in range(4 + (i == 2)) ])(eval(self.teamup[teamName]['masterSkill'])) def saveTeam(self): if not self.CBX_TEAM.currentText(): return self.teamup[self.CBX_TEAM.currentText()] = { 'teamIndex': self.TXT_TEAM.value(), 'skillInfo': str([[[ getattr(self, f'TXT_SKILL_{i}_{j}_{k}').value() for k in range(4) ] for j in range(3)] for i in range(6)]).replace(' ', ''), 'houguInfo': str([[ getattr(self, f'TXT_HOUGU_{i}_{j}').value() for j in range(2) ] for i in range(6)]).replace(' ', ''), 'masterSkill': str([[ getattr(self, f'TXT_MASTER_{i}_{j}').value() for j in range(4 + (i == 2)) ] for i in range(3)]).replace(' ', '') } with open('fgoTeamup.ini', 'w') as f: self.teamup.write(f) def resetTeam(self): self.loadTeam('DEFAULT') def getDevice(self): text, ok = QInputDialog.getItem( self, 'FGO-py', '在下拉列表中选择一个设备', l := fgoKernel.Device.enumDevices(), l.index(fgoKernel.device.name) if fgoKernel.device.name and fgoKernel.device.name in l else 0, True, Qt.WindowType.WindowStaysOnTopHint) if not ok: return fgoKernel.device = fgoKernel.Device(text) self.LBL_DEVICE.setText(fgoKernel.device.name) self.MENU_CONTROL_MAPKEY.setChecked(False) def runBattle(self): self.runFunc(fgoKernel.Battle()) def runUserScript(self): self.runFunc(fgoKernel.UserScript()) def runMain(self): text, ok = QInputDialog.getItem(self, '肝哪个', '在下拉列表中选择战斗函数', ['完成战斗', '用户脚本'], 0, False) if ok and text: self.runFunc( fgoKernel.Main(self.TXT_APPLE.value(), self.CBX_APPLE.currentIndex(), { '完成战斗': fgoKernel.Battle, '用户脚本': fgoKernel.UserScript }[text])) def pause(self, x): if not x and not self.isDeviceAvailable(): return self.BTN_PAUSE.setChecked(True) fgoKernel.schedule.pause() def stop(self): fgoKernel.schedule.stop('Terminate Command Effected') def stopLater(self, x): if x: num, ok = QInputDialog.getInt(self, '输入', '剩余的战斗数量', 1, 1, 1919810, 1) if ok: fgoKernel.schedule.stopLater(num) else: self.BTN_STOPLATER.setChecked(False) else: fgoKernel.schedule.stopLater() def checkScreenshot(self): if not self.isDeviceAvailable(): return try: fgoKernel.Detect(0, blockFuse=True).show() except Exception as e: logger.exception(e) def applyAll(self): fgoKernel.Main.teamIndex = self.TXT_TEAM.value() fgoKernel.Turn.skillInfo = [[[ getattr(self, f'TXT_SKILL_{i}_{j}_{k}').value() for k in range(4) ] for j in range(3)] for i in range(6)] fgoKernel.Turn.houguInfo = [[ getattr(self, f'TXT_HOUGU_{i}_{j}').value() for j in range(2) ] for i in range(6)] fgoKernel.Turn.masterSkill = [[ getattr(self, f'TXT_MASTER_{i}_{j}').value() for j in range(4 + (i == 2)) ] for i in range(3)] def explorerHere(self): os.startfile('.') def runGacha(self): self.runFunc(fgoKernel.gacha) def runJackpot(self): self.runFunc(fgoKernel.jackpot) def runMail(self): self.runFunc(fgoKernel.mail) def runSynthesis(self): self.runFunc(fgoKernel.synthesis) def stopOnDefeated(self, x): self.config['stopOnDefeated'] = x def stopOnKizunaReisou(self, x): self.config['stopOnKizunaReisou'] = x def stopOnSpecialDrop(self): num, ok = QInputDialog.getInt(self, '输入', '剩余的特殊掉落数量', 1, 0, 1919810, 1) if ok: fgoKernel.schedule.stopOnSpecialDrop(num) def stayOnTop(self, x): self.config['stayOnTop'] = x self.show() def closeToTray(self, x): self.config['closeToTray'] = x def reloadTeamup(self): self.teamup = IniParser('fgoTeamup.ini') self.CBX_TEAM.clear() self.CBX_TEAM.addItems(self.teamup.sections()) self.CBX_TEAM.setCurrentIndex(-1) self.loadTeam('DEFAULT') def mapKey(self, x): self.MENU_CONTROL_MAPKEY.setChecked(x and self.isDeviceAvailable()) def invoke169(self): if not self.isDeviceAvailable(): return fgoKernel.device.invoke169() def revoke169(self): if not self.isDeviceAvailable(): return fgoKernel.device.revoke169() def notify(self, x): self.config['notifyEnable'] = x def exec(self): s = QApplication.clipboard().text() if QMessageBox.information( self, 'FGO-py', s, QMessageBox.StandardButton.Ok | QMessageBox. StandardButton.Cancel) != QMessageBox.StandardButton.Ok: return try: exec(s) except BaseException as e: logger.exception(e) def about(self): QMessageBox.about( self, 'FGO-py - About', f''' <h2>FGO-py</h2> FGO全自动脚本 <table border="0"> <tr><td>当前版本</td><td>{fgoKernel.__version__}</td></tr> <tr><td>作者</td><td>hgjazhgj</td></tr> <tr><td>项目地址</td><td><a href="https://github.com/hgjazhgj/FGO-py">https://github.com/hgjazhgj/FGO-py</a></td></tr> <tr><td>电子邮箱</td><td><a href="mailto:[email protected]">[email protected]</a></td></tr> <tr><td>QQ群</td><td>932481680</td></tr> </table> <!-- 都看到这里了真的不考虑资瓷一下吗... --> 这是我的<font color="#00A0E8">支付宝</font>/<font color="#22AB38">微信</font>收款码和Monero地址<br/>请给我打钱<br/> <img height="116" width="116" src="data:image/bmp;base64,Qk2yAAAAAAAAAD4AAAAoAAAAHQAAAB0AAAABAAEAAAAAAHQAAAB0EgAAdBIAAAAAAAAAAAAA6KAAAP///wABYWKofU/CKEV/ZtBFXEMwRbiQUH2a5yABj+Uo/zf3AKDtsBjeNa7YcUYb2MrQ04jEa/Ioh7TO6BR150Djjo3ATKgPmGLjdfDleznImz0gcA19mxD/rx/4AVVUAH2zpfBFCgUQRSgtEEVjdRB9/R3wATtkAA=="/> <img height="116" width="116" src="data:image/bmp;base64,Qk2yAAAAAAAAAD4AAAAoAAAAHQAAAB0AAAABAAEAAAAAAHQAAAB0EgAAdBIAAAAAAAAAAAAAOKsiAP///wABNLhYfVLBqEUYG0hFcn7gRS8QAH2Pd2ABQiVY/x1nMFWzcFhidNUwaXr3GEp1khDJzDfAuqx06ChC9hhPvmIQMJX3SCZ13ehlXB9IVtJQUAQreqj/jv/4AVVUAH0iFfBFuxUQRRAlEEX2fRB9Wl3wAdBsAA=="/> <table border="0"><tr> <td><img height="148" width="148" src="data:image/bmp;base64,Qk1mAQAAAAAAAD4AAAAoAAAAJQAAACUAAAABAAEAAAAAACgBAAB0EgAAdBIAAAAAAAAAAAAAAAAAAP///wABNpugAAAAAH0Q2oL4AAAARb1nmkAAAABFZnR3IAAAAEXpv9AwAAAAfZSA10AAAAABXdMVYAAAAP8qTsdQAAAAMd998EgAAACighiQeAAAAFCt3LiwAAAAo3aTXIAAAACAQzl8SAAAAEehYzFgAAAAcZ0FlEAAAACmEjZXoAAAAD2l77w4AAAAvy27zoAAAAD4P5FWQAAAAEYVS3VwAAAAyXKhYYAAAACvQwA4OAAAALyhfNNwAAAAhuODSLAAAABIC/+BMAAAABpa6jMwAAAA6TltfQAAAAATihl8wAAAACzQ8IxIAAAA/zQAZ/gAAAABVVVUAAAAAH0qre3wAAAARXxupRAAAABFiJ3tEAAAAEUGtG0QAAAAfWa6DfAAAAABsL3cAAAAAA=="/></td> <td><font face="Courier New">42Cnr V9Tuz E1jiS<br/>2ucGw tzN8g F6o4y<br/>9SkHs X1eZE vtiDf<br/>4QcL1 NXvfZ PhDu7<br/>LYStW rbsQM 9UUGW<br/>nqXgh ManMB dqjEW<br/>5oaDY</font></td> </tr></table> ''') def license(self): os.system( f'start notepad {"LICENSE"if os.path.isfile("LICENSE")else"../LICENSE"}' )
class QtApplication(QApplication, Application): """Application subclass that provides a Qt application object.""" pluginsLoaded = Signal() applicationRunning = Signal() def __init__(self, tray_icon_name: str = None, **kwargs) -> None: self.setAttribute(Qt.ApplicationAttribute.AA_UseDesktopOpenGL) QQuickWindow.setGraphicsApi(QSGRendererInterface.GraphicsApi.OpenGL) plugin_path = "" if sys.platform == "win32": if hasattr(sys, "frozen"): plugin_path = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "PyQt6", "plugins") Logger.log("i", "Adding QT6 plugin path: %s", plugin_path) QCoreApplication.addLibraryPath(plugin_path) else: import site for sitepackage_dir in site.getsitepackages(): QCoreApplication.addLibraryPath(os.path.join(sitepackage_dir, "PyQt6", "plugins")) elif sys.platform == "darwin": plugin_path = os.path.join(self.getInstallPrefix(), "Resources", "plugins") if plugin_path: Logger.log("i", "Adding QT5 plugin path: %s", plugin_path) QCoreApplication.addLibraryPath(plugin_path) # use Qt Quick Scene Graph "basic" render loop os.environ["QSG_RENDER_LOOP"] = "basic" # Force using Fusion style for consistency between Windows, mac OS and Linux os.environ["QT_QUICK_CONTROLS_STYLE"] = "Fusion" super().__init__(sys.argv, **kwargs) self._qml_import_paths: List[str] = [] self._main_qml: str = "main.qml" self._qml_engine: Optional[QQmlApplicationEngine] = None self._main_window: Optional[MainWindow] = None self._tray_icon_name: Optional[str] = tray_icon_name self._tray_icon: Optional[str] = None self._tray_icon_widget: Optional[QSystemTrayIcon] = None self._theme: Optional[Theme] = None self._renderer: Optional[QtRenderer] = None self._job_queue: Optional[JobQueue] = None self._version_upgrade_manager: Optional[VersionUpgradeManager] = None self._is_shutting_down: bool = False self._recent_files: List[QUrl] = [] self._configuration_error_message: Optional[ConfigurationErrorMessage] = None self._http_network_request_manager: Optional[HttpRequestManager] = None #Metadata required for the file dialogues. self.setOrganizationDomain("https://ultimaker.com/") self.setOrganizationName("Ultimaker B.V.") def addCommandLineOptions(self) -> None: super().addCommandLineOptions() # This flag is used by QApplication. We don't process it. self._cli_parser.add_argument("-qmljsdebugger", help = "For Qt's QML debugger compatibility") def _isPathSecure(self, path: str) -> bool: install_prefix = os.path.abspath(self.getInstallPrefix()) return TrustBasics.isPathInLocation(install_prefix, path) def initialize(self, check_if_trusted: bool = False) -> None: super().initialize() preferences = Application.getInstance().getPreferences() if check_if_trusted: # Need to do this before the preferences are read for the first time, but after obj-creation, which is here. preferences.indicateUntrustedPreference("general", "theme", lambda value: self._isPathSecure(Resources.getPath(Resources.Themes, value))) preferences.indicateUntrustedPreference("backend", "location", lambda value: self._isPathSecure(os.path.abspath(value))) preferences.addPreference("view/force_empty_shader_cache", False) preferences.addPreference("view/opengl_version_detect", OpenGLContext.OpenGlVersionDetect.Autodetect) # Read preferences here (upgrade won't work) to get: # - The language in use, so the splash window can be shown in the correct language. # - The OpenGL 'force' parameters. try: self.readPreferencesFromConfiguration() except FileNotFoundError: Logger.log("i", "Preferences file not found, ignore and use default language '%s'", self._default_language) # Initialize the package manager to remove and install scheduled packages. self._package_manager = self._package_manager_class(self, parent = self) # If a plugin is removed, check if the matching package is also removed. self._plugin_registry.pluginRemoved.connect(lambda plugin_id: self._package_manager.removePackage(plugin_id)) self._mesh_file_handler = MeshFileHandler(self) #type: MeshFileHandler self._workspace_file_handler = WorkspaceFileHandler(self) #type: WorkspaceFileHandler if preferences.getValue("view/force_empty_shader_cache"): self.setAttribute(Qt.ApplicationAttribute.AA_DisableShaderDiskCache) if preferences.getValue("view/opengl_version_detect") != OpenGLContext.OpenGlVersionDetect.ForceModern: major_version, minor_version, profile = OpenGLContext.detectBestOpenGLVersion( preferences.getValue("view/opengl_version_detect") == OpenGLContext.OpenGlVersionDetect.ForceLegacy) else: Logger.info("Force 'modern' OpenGL (4.1 core) -- overrides 'force legacy opengl' preference.") major_version, minor_version, profile = (4, 1, QSurfaceFormat.OpenGLContextProfile.CoreProfile) if major_version is None or minor_version is None or profile is None: Logger.log("e", "Startup failed because OpenGL version probing has failed: tried to create a 2.0 and 4.1 context. Exiting") if not self.getIsHeadLess(): QMessageBox.critical(None, "Failed to probe OpenGL", "Could not probe OpenGL. This program requires OpenGL 2.0 or higher. Please check your video card drivers.") sys.exit(1) else: opengl_version_str = OpenGLContext.versionAsText(major_version, minor_version, profile) Logger.log("d", "Detected most suitable OpenGL context version: %s", opengl_version_str) if not self.getIsHeadLess(): OpenGLContext.setDefaultFormat(major_version, minor_version, profile = profile) self._qml_import_paths.append(os.path.join(os.path.dirname(sys.executable), "qml")) self._qml_import_paths.append(os.path.join(self.getInstallPrefix(), "Resources", "qml")) Logger.log("i", "Initializing job queue ...") self._job_queue = JobQueue() self._job_queue.jobFinished.connect(self._onJobFinished) Logger.log("i", "Initializing version upgrade manager ...") self._version_upgrade_manager = VersionUpgradeManager(self) def isQmlEngineInitialized(self) -> bool: return self._qml_engine_initialized def _displayLoadingPluginSplashMessage(self, plugin_id: Optional[str]) -> None: message = i18nCatalog("uranium").i18nc("@info:progress", "Loading plugins...") if plugin_id: message = i18nCatalog("uranium").i18nc("@info:progress", "Loading plugin {plugin_id}...").format(plugin_id = plugin_id) self.showSplashMessage(message) def startSplashWindowPhase(self) -> None: super().startSplashWindowPhase() i18n_catalog = i18nCatalog("uranium") self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Initializing package manager...")) self._package_manager.initialize() signal.signal(signal.SIGINT, signal.SIG_DFL) # This is done here as a lot of plugins require a correct gl context. If you want to change the framework, # these checks need to be done in your <framework>Application.py class __init__(). self._configuration_error_message = ConfigurationErrorMessage(self, i18n_catalog.i18nc("@info:status", "Your configuration seems to be corrupt."), lifetime = 0, title = i18n_catalog.i18nc("@info:title", "Configuration errors") ) # Remove, install, and then loading plugins self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Loading plugins...")) # Remove and install the plugins that have been scheduled self._plugin_registry.initializeBeforePluginsAreLoaded() self._plugin_registry.pluginLoadStarted.connect(self._displayLoadingPluginSplashMessage) self._loadPlugins() self._plugin_registry.pluginLoadStarted.disconnect(self._displayLoadingPluginSplashMessage) self._plugin_registry.checkRequiredPlugins(self.getRequiredPlugins()) self.pluginsLoaded.emit() self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Updating configuration...")) with self._container_registry.lockFile(): VersionUpgradeManager.getInstance().upgrade() # Load preferences again because before we have loaded the plugins, we don't have the upgrade routine for # the preferences file. Now that we have, load the preferences file again so it can be upgraded and loaded. self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Loading preferences...")) try: preferences_filename = Resources.getPath(Resources.Preferences, self._app_name + ".cfg") with open(preferences_filename, "r", encoding = "utf-8") as f: serialized = f.read() # This performs the upgrade for Preferences self._preferences.deserialize(serialized) self._preferences.setValue("general/plugins_to_remove", "") self._preferences.writeToFile(preferences_filename) except (EnvironmentError, UnicodeDecodeError): Logger.log("i", "The preferences file cannot be opened or it is corrupted, so we will use default values") self.processEvents() # Force the configuration file to be written again since the list of plugins to remove maybe changed try: self.readPreferencesFromConfiguration() except FileNotFoundError: Logger.log("i", "The preferences file '%s' cannot be found, will use default values", self._preferences_filename) self._preferences_filename = Resources.getStoragePath(Resources.Preferences, self._app_name + ".cfg") Logger.info("Completed loading preferences.") # FIXME: This is done here because we now use "plugins.json" to manage plugins instead of the Preferences file, # but the PluginRegistry will still import data from the Preferences files if present, such as disabled plugins, # so we need to reset those values AFTER the Preferences file is loaded. self._plugin_registry.initializeAfterPluginsAreLoaded() # Check if we have just updated from an older version self._preferences.addPreference("general/last_run_version", "") last_run_version_str = self._preferences.getValue("general/last_run_version") if not last_run_version_str: last_run_version_str = self._version last_run_version = Version(last_run_version_str) current_version = Version(self._version) if last_run_version < current_version: self._just_updated_from_old_version = True self._preferences.setValue("general/last_run_version", str(current_version)) self._preferences.writeToFile(self._preferences_filename) # Preferences: recent files self._preferences.addPreference("%s/recent_files" % self._app_name, "") file_names = self._preferences.getValue("%s/recent_files" % self._app_name).split(";") for file_name in file_names: if not os.path.isfile(file_name): continue self._recent_files.append(QUrl.fromLocalFile(file_name)) if not self.getIsHeadLess(): # Initialize System tray icon and make it invisible because it is used only to show pop up messages self._tray_icon = None if self._tray_icon_name: try: self._tray_icon = QIcon(Resources.getPath(Resources.Images, self._tray_icon_name)) self._tray_icon_widget = QSystemTrayIcon(self._tray_icon) self._tray_icon_widget.setVisible(False) Logger.info("Created system tray icon.") except FileNotFoundError: Logger.log("w", "Could not find the icon %s", self._tray_icon_name) def readPreferencesFromConfiguration(self) -> None: self._preferences_filename = Resources.getPath(Resources.Preferences, self._app_name + ".cfg") self._preferences.readFromFile(self._preferences_filename) def initializeEngine(self) -> None: # TODO: Document native/qml import trickery self._qml_engine = QQmlApplicationEngine(self) self.processEvents() self._qml_engine.setOutputWarningsToStandardError(False) self._qml_engine.warnings.connect(self.__onQmlWarning) for path in self._qml_import_paths: self._qml_engine.addImportPath(path) if not hasattr(sys, "frozen"): self._qml_engine.addImportPath(os.path.join(os.path.dirname(__file__), "qml")) self._qml_engine.rootContext().setContextProperty("QT_VERSION_STR", QT_VERSION_STR) self.processEvents() self._qml_engine.rootContext().setContextProperty("screenScaleFactor", self._screenScaleFactor()) self.registerObjects(self._qml_engine) Bindings.register() # Preload theme. The theme will be loaded on first use, which will incur a ~0.1s freeze on the MainThread. # Do it here, while the splash screen is shown. Also makes this freeze explicit and traceable. self.getTheme() self.processEvents() i18n_catalog = i18nCatalog("uranium") self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Loading UI...")) self._qml_engine.load(self._main_qml) self._qml_engine_initialized = True self.engineCreatedSignal.emit() recentFilesChanged = pyqtSignal() @pyqtProperty("QVariantList", notify=recentFilesChanged) def recentFiles(self) -> List[QUrl]: return self._recent_files fileProvidersChanged = pyqtSignal() @pyqtProperty("QVariantList", notify = fileProvidersChanged) def fileProviders(self) -> List[FileProvider]: return self.getFileProviders() def _onJobFinished(self, job: Job) -> None: if isinstance(job, WriteFileJob) and not job.getResult(): return if isinstance(job, (ReadMeshJob, ReadFileJob, WriteFileJob)) and job.getAddToRecentFiles(): self.addFileToRecentFiles(job.getFileName()) def addFileToRecentFiles(self, file_name: str) -> None: file_path = QUrl.fromLocalFile(file_name) if file_path in self._recent_files: self._recent_files.remove(file_path) self._recent_files.insert(0, file_path) if len(self._recent_files) > 10: del self._recent_files[10] pref = "" for path in self._recent_files: pref += path.toLocalFile() + ";" self.getPreferences().setValue("%s/recent_files" % self.getApplicationName(), pref) self.recentFilesChanged.emit() def run(self) -> None: super().run() def hideMessage(self, message: Message) -> None: with self._message_lock: if message in self._visible_messages: message.hide(send_signal = False) # we're in handling hideMessageSignal so we don't want to resend it self._visible_messages.remove(message) self.visibleMessageRemoved.emit(message) def showMessage(self, message: Message) -> None: with self._message_lock: if message not in self._visible_messages: self._visible_messages.append(message) message.setLifetimeTimer(QTimer()) message.setInactivityTimer(QTimer()) self.visibleMessageAdded.emit(message) # also show toast message when the main window is minimized self.showToastMessage(self._app_name, message.getText()) def _onMainWindowStateChanged(self, window_state: int) -> None: if self._tray_icon and self._tray_icon_widget: visible = window_state == Qt.WindowState.WindowMinimized self._tray_icon_widget.setVisible(visible) # Show toast message using System tray widget. def showToastMessage(self, title: str, message: str) -> None: if self.checkWindowMinimizedState() and self._tray_icon_widget: # NOTE: Qt 5.8 don't support custom icon for the system tray messages, but Qt 5.9 does. # We should use the custom icon when we switch to Qt 5.9 self._tray_icon_widget.showMessage(title, message) def setMainQml(self, path: str) -> None: self._main_qml = path def exec(self, *args: Any, **kwargs: Any) -> None: self.applicationRunning.emit() super().exec(*args, **kwargs) @pyqtSlot() def reloadQML(self) -> None: # only reload when it is a release build if not self.getIsDebugMode(): return if self._qml_engine and self._theme: self._qml_engine.clearComponentCache() self._theme.reload() self._qml_engine.load(self._main_qml) # Hide the window. For some reason we can't close it yet. This needs to be done in the onComponentCompleted. for obj in self._qml_engine.rootObjects(): if obj != self._qml_engine.rootObjects()[-1]: obj.hide() @pyqtSlot() def purgeWindows(self) -> None: # Close all root objects except the last one. # Should only be called by onComponentCompleted of the mainWindow. if self._qml_engine: for obj in self._qml_engine.rootObjects(): if obj != self._qml_engine.rootObjects()[-1]: obj.close() @pyqtSlot("QList<QQmlError>") def __onQmlWarning(self, warnings: List[QQmlError]) -> None: for warning in warnings: Logger.log("w", warning.toString()) engineCreatedSignal = Signal() def isShuttingDown(self) -> bool: return self._is_shutting_down def registerObjects(self, engine) -> None: #type: ignore #Don't type engine, because the type depends on the platform you're running on so it always gives an error somewhere. engine.rootContext().setContextProperty("PluginRegistry", PluginRegistry.getInstance()) def getRenderer(self) -> QtRenderer: if not self._renderer: self._renderer = QtRenderer() return cast(QtRenderer, self._renderer) mainWindowChanged = Signal() def getMainWindow(self) -> Optional[MainWindow]: return self._main_window def setMainWindow(self, window: MainWindow) -> None: if window != self._main_window: if self._main_window is not None: self._main_window.windowStateChanged.disconnect(self._onMainWindowStateChanged) self._main_window = window if self._main_window is not None: self._main_window.windowStateChanged.connect(self._onMainWindowStateChanged) self.mainWindowChanged.emit() def setVisible(self, visible: bool) -> None: if self._main_window is not None: self._main_window.visible = visible @property def isVisible(self) -> bool: if self._main_window is not None: return self._main_window.isVisible() #type: ignore #MyPy doesn't realise that self._main_window cannot be None here. return False def getTheme(self) -> Optional[Theme]: if self._theme is None: if self._qml_engine is None: Logger.log("e", "The theme cannot be accessed before the engine is initialised") return None self._theme = UM.Qt.Bindings.Theme.Theme.getInstance(self._qml_engine) return self._theme # Handle a function that should be called later. def functionEvent(self, event: QEvent) -> None: e = _QtFunctionEvent(event) QCoreApplication.postEvent(self, e) # Handle Qt events def event(self, event: QEvent) -> bool: if event.type() == _QtFunctionEvent.QtFunctionEvent: event._function_event.call() return True return super().event(event) def windowClosed(self, save_data: bool = True) -> None: Logger.log("d", "Shutting down %s", self.getApplicationName()) self._is_shutting_down = True # garbage collect tray icon so it gets properly closed before the application is closed self._tray_icon_widget = None if save_data: try: self.savePreferences() except Exception as e: Logger.log("e", "Exception while saving preferences: %s", repr(e)) try: self.applicationShuttingDown.emit() except Exception as e: Logger.log("e", "Exception while emitting shutdown signal: %s", repr(e)) try: self.getBackend().close() except Exception as e: Logger.log("e", "Exception while closing backend: %s", repr(e)) if self._qml_engine: self._qml_engine.deleteLater() if self._tray_icon_widget: self._tray_icon_widget.deleteLater() self.quit() def checkWindowMinimizedState(self) -> bool: if self._main_window is not None and self._main_window.windowState() == Qt.WindowState.WindowMinimized: return True else: return False @pyqtSlot(result = "QObject*") def getBackend(self) -> Backend: """Get the backend of the application (the program that does the heavy lifting). The backend is also a QObject, which can be used from qml. """ return self._backend @pyqtProperty("QVariant", constant = True) def backend(self) -> Backend: """Property used to expose the backend It is made static as the backend is not supposed to change during runtime. This makes the connection between backend and QML more reliable than the pyqtSlot above. :returns: Backend :type{Backend} """ return self.getBackend() splash: Optional[QSplashScreen] = None """Create a class variable so we can manage the splash in the CrashHandler dialog when the Application instance is not yet created, e.g. when an error occurs during the initialization """ def createSplash(self) -> None: if not self.getIsHeadLess(): try: QtApplication.splash = self._createSplashScreen() except FileNotFoundError: QtApplication.splash = None else: if QtApplication.splash: QtApplication.splash.show() self.processEvents() def showSplashMessage(self, message: str) -> None: """Display text on the splash screen.""" if not QtApplication.splash: self.createSplash() if QtApplication.splash: self.processEvents() # Process events from previous loading phase before updating the message QtApplication.splash.showMessage(message, Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignVCenter) # Now update the message self.processEvents() # And make sure it is immediately visible elif self.getIsHeadLess(): Logger.log("d", message) def closeSplash(self) -> None: """Close the splash screen after the application has started.""" if QtApplication.splash: QtApplication.splash.close() QtApplication.splash = None def createQmlComponent(self, qml_file_path: str, context_properties: Dict[str, "QObject"] = None) -> Optional["QObject"]: """Create a QML component from a qml file. :param qml_file_path:: The absolute file path to the root qml file. :param context_properties:: Optional dictionary containing the properties that will be set on the context of the qml instance before creation. :return: None in case the creation failed (qml error), else it returns the qml instance. :note If the creation fails, this function will ensure any errors are logged to the logging service. """ if self._qml_engine is None: # Protect in case the engine was not initialized yet return None path = QUrl.fromLocalFile(qml_file_path) component = QQmlComponent(self._qml_engine, path) result_context = QQmlContext(self._qml_engine.rootContext()) #type: ignore #MyPy doens't realise that self._qml_engine can't be None here. if context_properties is not None: for name, value in context_properties.items(): result_context.setContextProperty(name, value) result = component.create(result_context) for err in component.errors(): Logger.log("e", str(err.toString())) if result is None: return None # We need to store the context with the qml object, else the context gets garbage collected and the qml objects # no longer function correctly/application crashes. result.attached_context = result_context return result @pyqtSlot() def deleteAll(self, only_selectable = True) -> None: """Delete all nodes containing mesh data in the scene. :param only_selectable:. Set this to False to delete objects from all build plates """ self.getController().deleteAllNodesWithMeshData(only_selectable) @pyqtSlot() def resetWorkspace(self) -> None: self._workspace_metadata_storage.clear() self._current_workspace_information.clear() self.deleteAll() self.workspaceLoaded.emit("") self.getController().getScene().clearMetaData() def getMeshFileHandler(self) -> MeshFileHandler: """Get the MeshFileHandler of this application.""" return self._mesh_file_handler def getWorkspaceFileHandler(self) -> WorkspaceFileHandler: return self._workspace_file_handler @pyqtSlot(result = QObject) def getPackageManager(self) -> PackageManager: return self._package_manager def getHttpRequestManager(self) -> "HttpRequestManager": if not self._http_network_request_manager: self._http_network_request_manager = HttpRequestManager.getInstance(parent = self) return self._http_network_request_manager @classmethod def getInstance(cls, *args, **kwargs) -> "QtApplication": """Gets the instance of this application. This is just to further specify the type of Application.getInstance(). :return: The instance of this application. """ return cast(QtApplication, super().getInstance(**kwargs)) def _createSplashScreen(self) -> QSplashScreen: return QSplashScreen(QPixmap(Resources.getPath(Resources.Images, self.getApplicationName() + ".png"))) def _screenScaleFactor(self) -> float: # OSX handles sizes of dialogs behind our backs, but other platforms need # to know about the device pixel ratio if sys.platform == "darwin": return 1.0 else: # determine a device pixel ratio from font metrics, using the same logic as UM.Theme fontPixelRatio = QFontMetrics(QCoreApplication.instance().font()).ascent() / 11 # round the font pixel ratio to quarters fontPixelRatio = int(fontPixelRatio * 4) / 4 return fontPixelRatio @pyqtProperty(str, constant=True) def applicationDisplayName(self) -> str: return self.getApplicationDisplayName()
def startSplashWindowPhase(self) -> None: super().startSplashWindowPhase() i18n_catalog = i18nCatalog("uranium") self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Initializing package manager...")) self._package_manager.initialize() signal.signal(signal.SIGINT, signal.SIG_DFL) # This is done here as a lot of plugins require a correct gl context. If you want to change the framework, # these checks need to be done in your <framework>Application.py class __init__(). self._configuration_error_message = ConfigurationErrorMessage(self, i18n_catalog.i18nc("@info:status", "Your configuration seems to be corrupt."), lifetime = 0, title = i18n_catalog.i18nc("@info:title", "Configuration errors") ) # Remove, install, and then loading plugins self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Loading plugins...")) # Remove and install the plugins that have been scheduled self._plugin_registry.initializeBeforePluginsAreLoaded() self._plugin_registry.pluginLoadStarted.connect(self._displayLoadingPluginSplashMessage) self._loadPlugins() self._plugin_registry.pluginLoadStarted.disconnect(self._displayLoadingPluginSplashMessage) self._plugin_registry.checkRequiredPlugins(self.getRequiredPlugins()) self.pluginsLoaded.emit() self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Updating configuration...")) with self._container_registry.lockFile(): VersionUpgradeManager.getInstance().upgrade() # Load preferences again because before we have loaded the plugins, we don't have the upgrade routine for # the preferences file. Now that we have, load the preferences file again so it can be upgraded and loaded. self.showSplashMessage(i18n_catalog.i18nc("@info:progress", "Loading preferences...")) try: preferences_filename = Resources.getPath(Resources.Preferences, self._app_name + ".cfg") with open(preferences_filename, "r", encoding = "utf-8") as f: serialized = f.read() # This performs the upgrade for Preferences self._preferences.deserialize(serialized) self._preferences.setValue("general/plugins_to_remove", "") self._preferences.writeToFile(preferences_filename) except (EnvironmentError, UnicodeDecodeError): Logger.log("i", "The preferences file cannot be opened or it is corrupted, so we will use default values") self.processEvents() # Force the configuration file to be written again since the list of plugins to remove maybe changed try: self.readPreferencesFromConfiguration() except FileNotFoundError: Logger.log("i", "The preferences file '%s' cannot be found, will use default values", self._preferences_filename) self._preferences_filename = Resources.getStoragePath(Resources.Preferences, self._app_name + ".cfg") Logger.info("Completed loading preferences.") # FIXME: This is done here because we now use "plugins.json" to manage plugins instead of the Preferences file, # but the PluginRegistry will still import data from the Preferences files if present, such as disabled plugins, # so we need to reset those values AFTER the Preferences file is loaded. self._plugin_registry.initializeAfterPluginsAreLoaded() # Check if we have just updated from an older version self._preferences.addPreference("general/last_run_version", "") last_run_version_str = self._preferences.getValue("general/last_run_version") if not last_run_version_str: last_run_version_str = self._version last_run_version = Version(last_run_version_str) current_version = Version(self._version) if last_run_version < current_version: self._just_updated_from_old_version = True self._preferences.setValue("general/last_run_version", str(current_version)) self._preferences.writeToFile(self._preferences_filename) # Preferences: recent files self._preferences.addPreference("%s/recent_files" % self._app_name, "") file_names = self._preferences.getValue("%s/recent_files" % self._app_name).split(";") for file_name in file_names: if not os.path.isfile(file_name): continue self._recent_files.append(QUrl.fromLocalFile(file_name)) if not self.getIsHeadLess(): # Initialize System tray icon and make it invisible because it is used only to show pop up messages self._tray_icon = None if self._tray_icon_name: try: self._tray_icon = QIcon(Resources.getPath(Resources.Images, self._tray_icon_name)) self._tray_icon_widget = QSystemTrayIcon(self._tray_icon) self._tray_icon_widget.setVisible(False) Logger.info("Created system tray icon.") except FileNotFoundError: Logger.log("w", "Could not find the icon %s", self._tray_icon_name)
def __init__(self, main_window): QSystemTrayIcon.__init__(self) self.main_window = main_window self.setIcon(QIcon(LOGO_PADDED_PATH)) self.initialize_context_menu()