Esempio n. 1
0
 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()
Esempio n. 2
0
 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
Esempio n. 3
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()
Esempio n. 4
0
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()
Esempio n. 5
0
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"}'
        )
Esempio n. 6
0
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()
Esempio n. 7
0
    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)
Esempio n. 8
0
 def __init__(self, main_window):
     QSystemTrayIcon.__init__(self)
     self.main_window = main_window
     self.setIcon(QIcon(LOGO_PADDED_PATH))
     self.initialize_context_menu()