Пример #1
0
class ModernWindow(QWidget):
    """ Modern window.

        Args:
            w (QWidget): Main widget.
            parent (QWidget, optional): Parent widget.
    """
    def __init__(self, w, parent=None):
        QWidget.__init__(self, parent)

        self._w = w
        self.setupUi()

        contentLayout = QHBoxLayout()
        contentLayout.setContentsMargins(0, 0, 0, 0)
        contentLayout.addWidget(w)

        self.windowContent.setLayout(contentLayout)

        self.setWindowTitle(w.windowTitle())
        self.setGeometry(w.geometry())

        # Adding attribute to clean up the parent window when the child is closed
        self._w.setAttribute(Qt.WA_DeleteOnClose, True)
        self._w.destroyed.connect(self.__child_was_closed)

    def setupUi(self):
        # create title bar, content
        self.vboxWindow = QVBoxLayout(self)
        self.vboxWindow.setContentsMargins(0, 0, 0, 0)

        self.windowFrame = QWidget(self)
        self.windowFrame.setObjectName('windowFrame')

        self.vboxFrame = QVBoxLayout(self.windowFrame)
        self.vboxFrame.setContentsMargins(0, 0, 0, 0)

        self.titleBar = WindowDragger(self, self.windowFrame)
        self.titleBar.setObjectName('titleBar')
        self.titleBar.setSizePolicy(
            QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed))

        self.hboxTitle = QHBoxLayout(self.titleBar)
        self.hboxTitle.setContentsMargins(0, 0, 0, 0)
        self.hboxTitle.setSpacing(0)

        self.lblTitle = QLabel('Title')
        self.lblTitle.setObjectName('lblTitle')
        self.lblTitle.setAlignment(Qt.AlignCenter)

        spButtons = QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)

        self.btnMinimize = QToolButton(self.titleBar)
        self.btnMinimize.setObjectName('btnMinimize')
        self.btnMinimize.setSizePolicy(spButtons)

        self.btnRestore = QToolButton(self.titleBar)
        self.btnRestore.setObjectName('btnRestore')
        self.btnRestore.setSizePolicy(spButtons)

        self.btnMaximize = QToolButton(self.titleBar)
        self.btnMaximize.setObjectName('btnMaximize')
        self.btnMaximize.setSizePolicy(spButtons)

        self.btnClose = QToolButton(self.titleBar)
        self.btnClose.setObjectName('btnClose')
        self.btnClose.setSizePolicy(spButtons)

        self.vboxFrame.addWidget(self.titleBar)

        self.windowContent = QWidget(self.windowFrame)
        self.vboxFrame.addWidget(self.windowContent)

        self.vboxWindow.addWidget(self.windowFrame)

        if PLATFORM == "Darwin":
            self.hboxTitle.addWidget(self.btnClose)
            self.hboxTitle.addWidget(self.btnMinimize)
            self.hboxTitle.addWidget(self.btnRestore)
            self.hboxTitle.addWidget(self.btnMaximize)
            self.hboxTitle.addWidget(self.lblTitle)
        else:
            self.hboxTitle.addWidget(self.lblTitle)
            self.hboxTitle.addWidget(self.btnMinimize)
            self.hboxTitle.addWidget(self.btnRestore)
            self.hboxTitle.addWidget(self.btnMaximize)
            self.hboxTitle.addWidget(self.btnClose)

        # set window flags
        self.setWindowFlags(Qt.Window | Qt.FramelessWindowHint
                            | Qt.WindowSystemMenuHint
                            | Qt.WindowCloseButtonHint
                            | Qt.WindowMinimizeButtonHint
                            | Qt.WindowMaximizeButtonHint)

        if QT_VERSION >= (5, ):
            self.setAttribute(Qt.WA_TranslucentBackground)

        # set stylesheet
        with open(_FL_STYLESHEET) as stylesheet:
            self.setStyleSheet(stylesheet.read())

        # automatically connect slots
        QMetaObject.connectSlotsByName(self)

    def __child_was_closed(self):
        self._w = None  # The child was deleted, remove the reference to it and close the parent window
        self.close()

    def closeEvent(self, event):
        if not self._w:
            event.accept()
        else:
            self._w.close()
            event.setAccepted(self._w.isHidden())

    def setWindowTitle(self, title):
        """ Set window title.

            Args:
                title (str): Title.
        """

        super(ModernWindow, self).setWindowTitle(title)
        self.lblTitle.setText(title)

    def _setWindowButtonState(self, hint, state):
        btns = {
            Qt.WindowCloseButtonHint: self.btnClose,
            Qt.WindowMinimizeButtonHint: self.btnMinimize,
            Qt.WindowMaximizeButtonHint: self.btnMaximize
        }
        button = btns.get(hint)

        maximized = bool(self.windowState() & Qt.WindowMaximized)

        if button == self.btnMaximize:  # special rules for max/restore
            self.btnRestore.setEnabled(state)
            self.btnMaximize.setEnabled(state)

            if maximized:
                self.btnRestore.setVisible(state)
                self.btnMaximize.setVisible(False)
            else:
                self.btnMaximize.setVisible(state)
                self.btnRestore.setVisible(False)
        else:
            button.setEnabled(state)

        allButtons = [
            self.btnClose, self.btnMinimize, self.btnMaximize, self.btnRestore
        ]
        if True in [b.isEnabled() for b in allButtons]:
            for b in allButtons:
                b.setVisible(True)
            if maximized:
                self.btnMaximize.setVisible(False)
            else:
                self.btnRestore.setVisible(False)
            self.lblTitle.setContentsMargins(0, 0, 0, 0)
        else:
            for b in allButtons:
                b.setVisible(False)
            self.lblTitle.setContentsMargins(0, 2, 0, 0)

    def setWindowFlag(self, Qt_WindowType, on=True):
        buttonHints = [
            Qt.WindowCloseButtonHint, Qt.WindowMinimizeButtonHint,
            Qt.WindowMaximizeButtonHint
        ]

        if Qt_WindowType in buttonHints:
            self._setWindowButtonState(Qt_WindowType, on)
        else:
            QWidget.setWindowFlag(self, Qt_WindowType, on)

    def setWindowFlags(self, Qt_WindowFlags):
        buttonHints = [
            Qt.WindowCloseButtonHint, Qt.WindowMinimizeButtonHint,
            Qt.WindowMaximizeButtonHint
        ]
        for hint in buttonHints:
            self._setWindowButtonState(hint, bool(Qt_WindowFlags & hint))

        QWidget.setWindowFlags(self, Qt_WindowFlags)

    @Slot()
    def on_btnMinimize_clicked(self):
        self.setWindowState(Qt.WindowMinimized)

    @Slot()
    def on_btnRestore_clicked(self):
        if self.btnMaximize.isEnabled() or self.btnRestore.isEnabled():
            self.btnRestore.setVisible(False)
            self.btnRestore.setEnabled(False)
            self.btnMaximize.setVisible(True)
            self.btnMaximize.setEnabled(True)

        self.setWindowState(Qt.WindowNoState)

    @Slot()
    def on_btnMaximize_clicked(self):
        if self.btnMaximize.isEnabled() or self.btnRestore.isEnabled():
            self.btnRestore.setVisible(True)
            self.btnRestore.setEnabled(True)
            self.btnMaximize.setVisible(False)
            self.btnMaximize.setEnabled(False)

        self.setWindowState(Qt.WindowMaximized)

    @Slot()
    def on_btnClose_clicked(self):
        self.close()

    @Slot()
    def on_titleBar_doubleClicked(self):
        if not bool(self.windowState() & Qt.WindowMaximized):
            self.on_btnMaximize_clicked()
        else:
            self.on_btnRestore_clicked()
Пример #2
0
    def __init__(self,
                 parent: Optional[QWidget] = None,
                 firstStart: bool = False) -> None:
        super().__init__(parent, )

        if parent:
            self.setWindowTitle('Settings')
        else:
            self.setWindowTitle(getTitleString('Settings'))
            self.setAttribute(Qt.WA_DeleteOnClose)

        settings = QSettings()
        mainLayout = QVBoxLayout(self)
        mainLayout.setContentsMargins(5, 5, 5, 5)

        # First Start info

        if firstStart:
            firstStartInfo = QLabel(
                '''
                <p><strong>Hello! It looks like this is your first time using w3modmanager,
                or the game installation path recently changed.</strong></p>
                <p>
                Please review the settings below.
                </p>
                ''', self)
            firstStartInfo.setWordWrap(True)
            firstStartInfo.setContentsMargins(10, 10, 10, 10)
            firstStartInfo.setSizePolicy(QSizePolicy.Minimum,
                                         QSizePolicy.Minimum)
            mainLayout.addWidget(firstStartInfo)

        # Game

        gbGame = QGroupBox('Game Path', self)
        mainLayout.addWidget(gbGame)
        gbGameLayout = QVBoxLayout(gbGame)

        gamePathLayout = QHBoxLayout()
        self.gamePath = QLineEdit(self)
        self.gamePath.setPlaceholderText('Path to witcher3.exe...')
        if settings.value('gamePath'):
            self.gamePath.setText(str(settings.value('gamePath')))
        self.gamePath.textChanged.connect(
            lambda: self.validateGamePath(self.gamePath.text()))
        gamePathLayout.addWidget(self.gamePath)
        self.locateGame = QPushButton('Detect', self)
        self.locateGame.clicked.connect(self.locateGameEvent)
        self.locateGame.setToolTip(
            'Automatically detect the game path if possible')
        gamePathLayout.addWidget(self.locateGame)
        selectGame = QPushButton('Browse', self)
        selectGame.clicked.connect(self.selectGameEvent)
        gamePathLayout.addWidget(selectGame)
        gbGameLayout.addLayout(gamePathLayout)

        gamePathInfoLayout = QHBoxLayout()
        self.gamePathInfo = QLabel('', self)
        self.gamePathInfo.setContentsMargins(4, 4, 4, 4)
        self.gamePathInfo.setMinimumHeight(40)
        self.gamePathInfo.setWordWrap(True)
        gamePathInfoLayout.addWidget(self.gamePathInfo)
        gbGameLayout.addLayout(gamePathInfoLayout)

        # Config

        gbConfig = QGroupBox('Game Config', self)
        mainLayout.addWidget(gbConfig)
        gbConfigLayout = QVBoxLayout(gbConfig)

        configPathLayout = QHBoxLayout()
        self.configPath = QLineEdit(self)
        self.configPath.setPlaceholderText('Path to config folder...')
        if settings.value('configPath'):
            self.configPath.setText(str(settings.value('configPath')))
        self.configPath.textChanged.connect(
            lambda: self.validateConfigPath(self.configPath.text()))
        configPathLayout.addWidget(self.configPath)
        self.locateConfig = QPushButton('Detect', self)
        self.locateConfig.clicked.connect(self.locateConfigEvent)
        self.locateConfig.setToolTip(
            'Automatically detect the config folder if possible')
        configPathLayout.addWidget(self.locateConfig)
        selectConfig = QPushButton('Browse', self)
        selectConfig.clicked.connect(self.selectConfigEvent)
        configPathLayout.addWidget(selectConfig)
        gbConfigLayout.addLayout(configPathLayout)

        configPathInfoLayout = QHBoxLayout()
        self.configPathInfo = QLabel('', self)
        self.configPathInfo.setContentsMargins(4, 4, 4, 4)
        self.configPathInfo.setMinimumHeight(40)
        self.configPathInfo.setWordWrap(True)
        configPathInfoLayout.addWidget(self.configPathInfo)
        gbConfigLayout.addLayout(configPathInfoLayout)

        # Script Merger

        gbScriptMerger = QGroupBox('Script Merger', self)
        mainLayout.addWidget(gbScriptMerger)
        gbScriptMergerLayout = QVBoxLayout(gbScriptMerger)

        scriptMergerPathLayout = QHBoxLayout()
        self.scriptMergerPath = QLineEdit(self)
        self.scriptMergerPath.setPlaceholderText(
            'Path to WitcherScriptMerger.exe...')
        if settings.value('scriptMergerPath'):
            self.scriptMergerPath.setText(
                str(settings.value('scriptMergerPath')))
        self.scriptMergerPath.textChanged.connect(
            lambda: self.validateScriptMergerPath(self.scriptMergerPath.text()
                                                  ))
        scriptMergerPathLayout.addWidget(self.scriptMergerPath)
        self.locateScriptMerger = QPushButton('Detect', self)
        self.locateScriptMerger.clicked.connect(self.locateScriptMergerEvent)
        self.locateScriptMerger.setToolTip(
            'Automatically detect the script merger path if possible')
        scriptMergerPathLayout.addWidget(self.locateScriptMerger)
        selectScriptMerger = QPushButton('Browse', self)
        selectScriptMerger.clicked.connect(self.selectScriptMergerEvent)
        scriptMergerPathLayout.addWidget(selectScriptMerger)
        gbScriptMergerLayout.addLayout(scriptMergerPathLayout)

        scriptMergerPathInfoLayout = QHBoxLayout()
        self.scriptMergerPathInfo = QLabel('', self)
        self.scriptMergerPathInfo.setOpenExternalLinks(True)
        self.scriptMergerPathInfo.setContentsMargins(4, 4, 4, 4)
        self.scriptMergerPathInfo.setMinimumHeight(40)
        self.scriptMergerPathInfo.setWordWrap(True)
        scriptMergerPathInfoLayout.addWidget(self.scriptMergerPathInfo)
        gbScriptMergerLayout.addLayout(scriptMergerPathInfoLayout)

        # Nexus Mods API

        gbNexusModsAPI = QGroupBox('Nexus Mods API', self)
        mainLayout.addWidget(gbNexusModsAPI)
        gbNexusModsAPILayout = QVBoxLayout(gbNexusModsAPI)

        self.nexusAPIKey = QLineEdit(self)
        self.nexusAPIKey.setPlaceholderText('Personal API Key...')
        if settings.value('nexusAPIKey'):
            self.nexusAPIKey.setText(str(settings.value('nexusAPIKey')))
        self.nexusAPIKey.textChanged.connect(
            lambda: self.validateApiKey(self.nexusAPIKey.text()))
        gbNexusModsAPILayout.addWidget(self.nexusAPIKey)

        self.nexusAPIKeyInfo = QLabel('🌐', self)
        self.nexusAPIKeyInfo.setOpenExternalLinks(True)
        self.nexusAPIKeyInfo.setWordWrap(True)
        self.nexusAPIKeyInfo.setContentsMargins(4, 4, 4, 4)
        self.nexusAPIKeyInfo.setMinimumHeight(48)
        gbNexusModsAPILayout.addWidget(self.nexusAPIKeyInfo)

        self.nexusGetInfo = QCheckBox('Get Mod details after adding a new mod',
                                      self)
        self.nexusGetInfo.setChecked(
            settings.value('nexusGetInfo', 'True') == 'True')
        self.nexusGetInfo.setDisabled(True)
        gbNexusModsAPILayout.addWidget(self.nexusGetInfo)

        self.nexusCheckUpdates = QCheckBox('Check for Mod updates on startup',
                                           self)
        self.nexusCheckUpdates.setChecked(
            settings.value('nexusCheckUpdates', 'False') == 'True')
        self.nexusCheckUpdates.setDisabled(True)
        gbNexusModsAPILayout.addWidget(self.nexusCheckUpdates)

        self.nexusCheckClipboard = QCheckBox(
            'Monitor the Clipboard for Nexus Mods URLs', self)
        self.nexusCheckClipboard.setChecked(
            settings.value('nexusCheckClipboard', 'False') == 'True')
        self.nexusCheckClipboard.setDisabled(True)
        gbNexusModsAPILayout.addWidget(self.nexusCheckClipboard)

        # Output

        gbOutput = QGroupBox('Output Preferences', self)
        mainLayout.addWidget(gbOutput)
        gbOutputLayout = QVBoxLayout(gbOutput)
        self.unhideOutput = QCheckBox('Auto-show output panel', self)
        self.unhideOutput.setChecked(
            settings.value('unhideOutput', 'True') == 'True')
        gbOutputLayout.addWidget(self.unhideOutput)
        self.debugOutput = QCheckBox('Show debug output', self)
        self.debugOutput.setChecked(
            settings.value('debugOutput', 'False') == 'True')
        gbOutputLayout.addWidget(self.debugOutput)

        # Actions

        actionsLayout = QHBoxLayout()
        actionsLayout.setAlignment(Qt.AlignRight)
        self.save = QPushButton('Save', self)
        self.save.clicked.connect(self.saveEvent)
        self.save.setAutoDefault(True)
        self.save.setDefault(True)
        actionsLayout.addWidget(self.save)
        cancel = QPushButton('Cancel', self)
        cancel.clicked.connect(self.cancelEvent)
        actionsLayout.addWidget(cancel)
        mainLayout.addLayout(actionsLayout)

        # Setup

        if not settings.value('gamePath'):
            self.locateGameEvent()
        self.setMinimumSize(QSize(440, 440))
        self.setSizePolicy(QSizePolicy.MinimumExpanding,
                           QSizePolicy.MinimumExpanding)

        self.validGamePath = False
        self.validConfigPath = False
        self.validNexusAPIKey = False
        self.validScriptMergerPath = False

        self.validateGamePath(self.gamePath.text())
        self.validateConfigPath(self.configPath.text())
        self.validateApiKey(self.nexusAPIKey.text())
        self.validateScriptMergerPath(self.scriptMergerPath.text())
        self.updateSaveButton()

        self.finished.connect(
            lambda: self.validateApiKey.cancel())  # type: ignore
Пример #3
0
class MainWindow(QMainWindow, Ui_MainWindow):
    def __init__(self) -> None:
        super().__init__()
        self.setupUi(self)

        # Setting status bar
        self.statusLabel = QLabel()
        self.statusLabel.setContentsMargins(6, 0, 0, 6)
        self.statusBar.addWidget(self.statusLabel)

        # Set tables widget resizing policy
        header = self.proteinsTableWidget.horizontalHeader()
        header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(1, QHeaderView.ResizeToContents)

        header = self.peptidesTableWidget.horizontalHeader()
        header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(4, QHeaderView.ResizeToContents)

        header = self.subProteinsTableWidget.horizontalHeader()
        header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
        header.setSectionResizeMode(2, QHeaderView.ResizeToContents)

        # Variable holding the currently opened database
        self._database: Optional[DigestionDatabase] = None

        # Creating dialogs
        self._progress_dialog = QProgressDialog(self)
        self._progress_dialog.setWindowModality(Qt.WindowModal)
        self._progress_dialog.setMinimumDuration(200)
        self._progress_dialog.reset()

        # Creating an action group used in the working digestion menu
        self._working_digestion_action_group = QActionGroup(
            self.workingDigestionMenu)
        self._working_digestion_action_group.triggered.connect(
            self.workingDigestionMenuActionTriggered)

        # First refresh
        self.refreshMenusButtonsStatusBar()

    @property
    def database(self) -> DigestionDatabase:
        return self._database

    def _progressCallback(self, task: str, iteration: int,
                          maximum: int) -> bool:
        if iteration == -1:
            self._progress_dialog.close()
            self._progress_dialog.reset()
            return False
        else:
            self._progress_dialog.setLabelText(task)
            self._progress_dialog.setMaximum(maximum)
            self._progress_dialog.setValue(iteration)

            if not iteration:
                QApplication.processEvents()

            return self._progress_dialog.wasCanceled()

    def refreshMenusButtonsStatusBar(self, reset: bool = False) -> None:
        if not self._database:
            self.statusLabel.setText('No database opened')
        else:
            protein = f'{self._database.proteins_count} protein{"s" if self._database.proteins_count > 1 else ""}'
            sequence = f'{self._database.sequences_count} sequence{"s" if self._database.sequences_count > 1 else ""}'
            self.statusLabel.setText(', '.join(
                (str(self._database.path), protein, sequence)))

        if reset:
            self.proteinsSearchLineEdit.setText('')
            self.proteinsTableWidget.setRowCount(0)

        database_opened = bool(self._database)
        digestions_available = database_opened and bool(
            self._database.available_digestions)
        database_is_coherent = database_opened and bool(
            self._database.is_coherent_with_enzymes_collection)
        self.mainSplitter.setEnabled(database_opened)
        self.mainSplitterBottomWidget.setVisible(digestions_available)
        self.databaseMenu.setEnabled(database_opened and database_is_coherent)
        self.workingDigestionMenu.setEnabled(digestions_available)

        if digestions_available:
            if self._working_digestion_action_group.actions():
                current_digestion_settings = self._working_digestion_action_group.checkedAction(
                ).data()
            else:
                current_digestion_settings = None

            new_digestion_settings = None

            for action in self._working_digestion_action_group.actions():
                self._working_digestion_action_group.removeAction(action)
                action.deleteLater()

            for i, digestion in enumerate(self.database.available_digestions):
                action_title = (
                    f'{digestion.enzyme} - {digestion.missed_cleavages} missed cleavage'
                    f'{"s" if digestion.missed_cleavages > 1 else ""}')

                # Adding action to working digestion menu
                action = QAction(action_title,
                                 self._working_digestion_action_group)
                action.setCheckable(True)
                action.setData(digestion)
                self.workingDigestionMenu.addAction(action)

                if digestion == current_digestion_settings or not i:
                    new_digestion_settings = digestion
                    action.setChecked(True)

            # Refreshing if needed
            if current_digestion_settings != new_digestion_settings:
                self.refreshPeptidesTableWidget()

    def refreshProteinsTableWidget(self) -> None:
        search_text = self.proteinsSearchLineEdit.text().strip()
        search_mode = self.proteinsSearchTypeComboBox.currentIndex()

        if not search_text:
            return

        if search_mode == 0:
            results = self._database.search_proteins_by_name(
                search_text, limit=10000, callback=self._progressCallback)
        elif search_mode == 1:
            results = self._database.search_proteins_by_sequence(
                search_text, limit=10000, callback=self._progressCallback)
        elif search_mode == 2:
            try:
                digestion_settings = self._working_digestion_action_group.checkedAction(
                ).data()
            except AttributeError:
                results = []
            else:
                results = self._database.search_proteins_by_peptide_sequence(
                    search_text,
                    digestion_settings,
                    limit=10000,
                    callback=self._progressCallback)
        else:
            raise ValueError

        self.proteinsTableWidget.setRowCount(0)
        self.proteinsTableWidget.setSortingEnabled(False)

        try:
            for i, protein in enumerate(results):
                self.proteinsTableWidget.insertRow(i)
                index_item = QTableWidgetItem(str(i + 1).zfill(5))
                index_item.setData(TableItemDataRole.ROW_OBJECT, protein)
                name_item = QTableWidgetItem(protein.name)
                self.proteinsTableWidget.setItem(i, 0, index_item)
                self.proteinsTableWidget.setItem(i, 1, name_item)

        except ResultsLimitExceededError:
            commondialog.informationMessage(
                self, 'Your search returns too much results.\n'
                'Only the 10000 first results will be displayed.',
                dismissable=True)

        self.proteinsTableWidget.setSortingEnabled(True)
        self.proteinsTableWidget.resizeColumnToContents(-1)

        # Change search line edit text color to assure the user the search is done
        palette = self.proteinsSearchLineEdit.palette()

        if self.proteinsTableWidget.rowCount():
            palette.setColor(QPalette.Text, QColor(0, 180, 0))
        else:
            palette.setColor(QPalette.Text, QColor(180, 0, 0))

        self.proteinsSearchLineEdit.setPalette(palette)

    def refreshPeptidesTableWidget(self) -> None:
        selected_items = self.proteinsTableWidget.selectedItems()
        selected_protein = selected_items[0].data(
            TableItemDataRole.ROW_OBJECT) if selected_items else None
        selected_protein_id = selected_protein.id if selected_protein else None
        digestion_settings = self._working_digestion_action_group.checkedAction(
        ).data()

        if selected_protein_id and digestion_settings:
            results = self.database.search_peptides_by_protein_id(
                selected_protein_id,
                digestion_settings,
                limit=10000,
                callback=self._progressCallback)
        else:
            results = []

        self.peptidesTableWidget.setRowCount(0)
        self.peptidesTableWidget.setSortingEnabled(False)

        try:
            for i, peptide in enumerate(results):
                self.peptidesTableWidget.insertRow(i)
                index_item = QTableWidgetItem(str(i + 1).zfill(5))
                index_item.setData(TableItemDataRole.ROW_OBJECT, peptide)
                sequence_item = QTableWidgetItem(peptide.sequence)
                missed_cleavages_item = QTableWidgetItem(
                    str(peptide.missed_cleavages))
                digest_unique_item = QTableWidgetItem(
                    'Yes' if peptide.digest_unique else 'No')
                sequence_unique_item = QTableWidgetItem(
                    'Yes' if peptide.sequence_unique else 'No')
                self.peptidesTableWidget.setItem(i, 0, index_item)
                self.peptidesTableWidget.setItem(i, 1, sequence_item)
                self.peptidesTableWidget.setItem(i, 2, missed_cleavages_item)
                self.peptidesTableWidget.setItem(i, 3, digest_unique_item)
                self.peptidesTableWidget.setItem(i, 4, sequence_unique_item)

        except ResultsLimitExceededError:
            commondialog.informationMessage(
                self, 'Your search returns too much results.\n'
                'Only the 10000 first results will be displayed.',
                dismissable=True)

        self.peptidesTableWidget.setSortingEnabled(True)
        self.proteinsTableWidget.resizeColumnToContents(-1)

    def refreshSubProteinsTableWidget(self) -> None:
        selected_items = self.peptidesTableWidget.selectedItems()
        selected_peptide = selected_items[0].data(
            TableItemDataRole.ROW_OBJECT) if selected_items else None
        selected_peptide_id = selected_peptide.id if selected_peptide else None
        selected_peptide_sequence = selected_peptide.sequence if selected_peptide else None
        digestion_settings = self._working_digestion_action_group.checkedAction(
        ).data()
        by_id_results_ids_set = set()
        limit_reached = False

        self.subProteinsTableWidget.setRowCount(0)
        self.subProteinsTableWidget.setSortingEnabled(False)

        if selected_peptide_id:
            by_id_results = self.database.search_proteins_by_peptide_id(
                selected_peptide_id,
                digestion_settings,
                limit=10000,
                callback=self._progressCallback)
        else:
            by_id_results = []

        try:
            for i, protein in enumerate(by_id_results):
                self.subProteinsTableWidget.insertRow(i)
                index_item = QTableWidgetItem(str(i + 1).zfill(5))
                index_item.setData(TableItemDataRole.ROW_OBJECT, protein)
                name_item = QTableWidgetItem(protein.name)
                origin_item = QTableWidgetItem('by digest')
                self.subProteinsTableWidget.setItem(i, 0, index_item)
                self.subProteinsTableWidget.setItem(i, 1, name_item)
                self.subProteinsTableWidget.setItem(i, 2, origin_item)
                by_id_results_ids_set.add(protein.id)

        except ResultsLimitExceededError:
            commondialog.informationMessage(
                self, 'Your search returns too much results.\n'
                'Only the 10000 first results will be displayed.',
                dismissable=True)
            limit_reached = True

        if selected_peptide_sequence and not limit_reached:
            by_sequence_results = self.database.search_proteins_by_sequence(
                selected_peptide_sequence,
                limit=10000,
                callback=self._progressCallback)
        else:
            by_sequence_results = []

        try:
            for i, protein in enumerate(
                (filtered_protein for filtered_protein in by_sequence_results
                 if filtered_protein.id not in by_id_results_ids_set),
                    start=len(by_id_results_ids_set)):
                self.subProteinsTableWidget.insertRow(i)
                index_item = QTableWidgetItem(str(i + 1).zfill(5))
                index_item.setData(TableItemDataRole.ROW_OBJECT, protein)
                name_item = QTableWidgetItem(protein.name)
                origin_item = QTableWidgetItem('by sequence')
                self.subProteinsTableWidget.setItem(i, 0, index_item)
                self.subProteinsTableWidget.setItem(i, 1, name_item)
                self.subProteinsTableWidget.setItem(i, 2, origin_item)

        except ResultsLimitExceededError:
            commondialog.informationMessage(
                self, 'Your search returns too much by_id_results.\n'
                'Only the 10000 first results will be displayed.',
                dismissable=True)

        self.subProteinsTableWidget.setSortingEnabled(True)
        self.subProteinsTableWidget.resizeColumnToContents(-1)

    def createDatabaseActionTriggered(self) -> None:
        database_path = commondialog.fileSaveDialog(
            self,
            'Creating a database',
            filter='Digest database (*.digestdb)',
            extension='digestdb')

        if database_path:
            if self._database:
                self._database.close()

            self._database = DigestionDatabase(database_path, True, True)

        self.refreshMenusButtonsStatusBar(reset=True)

    def openDatabaseActionTriggered(self) -> None:
        database_path = commondialog.fileOpenDialog(
            self, 'Loading a database', filter='Digest database (*.digestdb)')

        if not database_path:
            return

        if self._database:
            self._database.close()

        self._database = DigestionDatabase(database_path)

        if not self._database.is_coherent_with_enzymes_collection:
            commondialog.informationMessage(
                self,
                'This database includes digestions done with enzymes that have '
                'been removed or modified in the enzymes files.\n'
                'Since this can lead to incoherent results, the import FASTA and '
                'manage database functions will be disabled.')

        self.refreshMenusButtonsStatusBar(reset=True)

    def importFastaActionTriggered(self) -> None:
        fasta_path = commondialog.fileOpenDialog(
            self, 'Importing a FASTA file', filter='FASTA database(*.fasta)')

        if fasta_path:
            self._database.import_database(fasta_path,
                                           callback=self._progressCallback)
            self.refreshMenusButtonsStatusBar()

    def manageDigestionActionTriggered(self) -> None:
        digestion_settings = DigestionDialog(self).run(self._database)

        if digestion_settings is not None:
            self._database.update_digestion(digestion_settings,
                                            remove=True,
                                            callback=self._progressCallback)
            self.refreshMenusButtonsStatusBar()

    def workingDigestionMenuActionTriggered(self, action) -> None:
        self.refreshPeptidesTableWidget()

    def proteinsSearchLineEditTextChanged(self, text):
        # Sets back text to standard color (in case it was set to red)
        self.proteinsSearchLineEdit.setPalette(
            QApplication.style().standardPalette())

    def proteinsSearchPushButtonClicked(self) -> None:
        self.refreshProteinsTableWidget()

    def proteinsTableWidgetItemSelectionChanged(self) -> None:
        self.peptidesTableWidget.setRowCount(0)

    def peptidesTableWidgetItemSelectionChanged(self) -> None:
        self.subProteinsTableWidget.setRowCount(0)

    def aboutActionTriggered(self) -> None:
        QMessageBox.about(
            self, 'About',
            f'{QApplication.applicationName()} {QApplication.applicationVersion()}'
        )
Пример #4
0
class SettingsWindow(QDialog):
    def __init__(self,
                 parent: Optional[QWidget] = None,
                 firstStart: bool = False) -> None:
        super().__init__(parent, )

        if parent:
            self.setWindowTitle('Settings')
        else:
            self.setWindowTitle(getTitleString('Settings'))
            self.setAttribute(Qt.WA_DeleteOnClose)

        settings = QSettings()
        mainLayout = QVBoxLayout(self)
        mainLayout.setContentsMargins(5, 5, 5, 5)

        # First Start info

        if firstStart:
            firstStartInfo = QLabel(
                '''
                <p><strong>Hello! It looks like this is your first time using w3modmanager,
                or the game installation path recently changed.</strong></p>
                <p>
                Please review the settings below.
                </p>
                ''', self)
            firstStartInfo.setWordWrap(True)
            firstStartInfo.setContentsMargins(10, 10, 10, 10)
            firstStartInfo.setSizePolicy(QSizePolicy.Minimum,
                                         QSizePolicy.Minimum)
            mainLayout.addWidget(firstStartInfo)

        # Game

        gbGame = QGroupBox('Game Path', self)
        mainLayout.addWidget(gbGame)
        gbGameLayout = QVBoxLayout(gbGame)

        gamePathLayout = QHBoxLayout()
        self.gamePath = QLineEdit(self)
        self.gamePath.setPlaceholderText('Path to witcher3.exe...')
        if settings.value('gamePath'):
            self.gamePath.setText(str(settings.value('gamePath')))
        self.gamePath.textChanged.connect(
            lambda: self.validateGamePath(self.gamePath.text()))
        gamePathLayout.addWidget(self.gamePath)
        self.locateGame = QPushButton('Detect', self)
        self.locateGame.clicked.connect(self.locateGameEvent)
        self.locateGame.setToolTip(
            'Automatically detect the game path if possible')
        gamePathLayout.addWidget(self.locateGame)
        selectGame = QPushButton('Browse', self)
        selectGame.clicked.connect(self.selectGameEvent)
        gamePathLayout.addWidget(selectGame)
        gbGameLayout.addLayout(gamePathLayout)

        gamePathInfoLayout = QHBoxLayout()
        self.gamePathInfo = QLabel('', self)
        self.gamePathInfo.setContentsMargins(4, 4, 4, 4)
        self.gamePathInfo.setMinimumHeight(40)
        self.gamePathInfo.setWordWrap(True)
        gamePathInfoLayout.addWidget(self.gamePathInfo)
        gbGameLayout.addLayout(gamePathInfoLayout)

        # Config

        gbConfig = QGroupBox('Game Config', self)
        mainLayout.addWidget(gbConfig)
        gbConfigLayout = QVBoxLayout(gbConfig)

        configPathLayout = QHBoxLayout()
        self.configPath = QLineEdit(self)
        self.configPath.setPlaceholderText('Path to config folder...')
        if settings.value('configPath'):
            self.configPath.setText(str(settings.value('configPath')))
        self.configPath.textChanged.connect(
            lambda: self.validateConfigPath(self.configPath.text()))
        configPathLayout.addWidget(self.configPath)
        self.locateConfig = QPushButton('Detect', self)
        self.locateConfig.clicked.connect(self.locateConfigEvent)
        self.locateConfig.setToolTip(
            'Automatically detect the config folder if possible')
        configPathLayout.addWidget(self.locateConfig)
        selectConfig = QPushButton('Browse', self)
        selectConfig.clicked.connect(self.selectConfigEvent)
        configPathLayout.addWidget(selectConfig)
        gbConfigLayout.addLayout(configPathLayout)

        configPathInfoLayout = QHBoxLayout()
        self.configPathInfo = QLabel('', self)
        self.configPathInfo.setContentsMargins(4, 4, 4, 4)
        self.configPathInfo.setMinimumHeight(40)
        self.configPathInfo.setWordWrap(True)
        configPathInfoLayout.addWidget(self.configPathInfo)
        gbConfigLayout.addLayout(configPathInfoLayout)

        # Script Merger

        gbScriptMerger = QGroupBox('Script Merger', self)
        mainLayout.addWidget(gbScriptMerger)
        gbScriptMergerLayout = QVBoxLayout(gbScriptMerger)

        scriptMergerPathLayout = QHBoxLayout()
        self.scriptMergerPath = QLineEdit(self)
        self.scriptMergerPath.setPlaceholderText(
            'Path to WitcherScriptMerger.exe...')
        if settings.value('scriptMergerPath'):
            self.scriptMergerPath.setText(
                str(settings.value('scriptMergerPath')))
        self.scriptMergerPath.textChanged.connect(
            lambda: self.validateScriptMergerPath(self.scriptMergerPath.text()
                                                  ))
        scriptMergerPathLayout.addWidget(self.scriptMergerPath)
        self.locateScriptMerger = QPushButton('Detect', self)
        self.locateScriptMerger.clicked.connect(self.locateScriptMergerEvent)
        self.locateScriptMerger.setToolTip(
            'Automatically detect the script merger path if possible')
        scriptMergerPathLayout.addWidget(self.locateScriptMerger)
        selectScriptMerger = QPushButton('Browse', self)
        selectScriptMerger.clicked.connect(self.selectScriptMergerEvent)
        scriptMergerPathLayout.addWidget(selectScriptMerger)
        gbScriptMergerLayout.addLayout(scriptMergerPathLayout)

        scriptMergerPathInfoLayout = QHBoxLayout()
        self.scriptMergerPathInfo = QLabel('', self)
        self.scriptMergerPathInfo.setOpenExternalLinks(True)
        self.scriptMergerPathInfo.setContentsMargins(4, 4, 4, 4)
        self.scriptMergerPathInfo.setMinimumHeight(40)
        self.scriptMergerPathInfo.setWordWrap(True)
        scriptMergerPathInfoLayout.addWidget(self.scriptMergerPathInfo)
        gbScriptMergerLayout.addLayout(scriptMergerPathInfoLayout)

        # Nexus Mods API

        gbNexusModsAPI = QGroupBox('Nexus Mods API', self)
        mainLayout.addWidget(gbNexusModsAPI)
        gbNexusModsAPILayout = QVBoxLayout(gbNexusModsAPI)

        self.nexusAPIKey = QLineEdit(self)
        self.nexusAPIKey.setPlaceholderText('Personal API Key...')
        if settings.value('nexusAPIKey'):
            self.nexusAPIKey.setText(str(settings.value('nexusAPIKey')))
        self.nexusAPIKey.textChanged.connect(
            lambda: self.validateApiKey(self.nexusAPIKey.text()))
        gbNexusModsAPILayout.addWidget(self.nexusAPIKey)

        self.nexusAPIKeyInfo = QLabel('🌐', self)
        self.nexusAPIKeyInfo.setOpenExternalLinks(True)
        self.nexusAPIKeyInfo.setWordWrap(True)
        self.nexusAPIKeyInfo.setContentsMargins(4, 4, 4, 4)
        self.nexusAPIKeyInfo.setMinimumHeight(48)
        gbNexusModsAPILayout.addWidget(self.nexusAPIKeyInfo)

        self.nexusGetInfo = QCheckBox('Get Mod details after adding a new mod',
                                      self)
        self.nexusGetInfo.setChecked(
            settings.value('nexusGetInfo', 'True') == 'True')
        self.nexusGetInfo.setDisabled(True)
        gbNexusModsAPILayout.addWidget(self.nexusGetInfo)

        self.nexusCheckUpdates = QCheckBox('Check for Mod updates on startup',
                                           self)
        self.nexusCheckUpdates.setChecked(
            settings.value('nexusCheckUpdates', 'False') == 'True')
        self.nexusCheckUpdates.setDisabled(True)
        gbNexusModsAPILayout.addWidget(self.nexusCheckUpdates)

        self.nexusCheckClipboard = QCheckBox(
            'Monitor the Clipboard for Nexus Mods URLs', self)
        self.nexusCheckClipboard.setChecked(
            settings.value('nexusCheckClipboard', 'False') == 'True')
        self.nexusCheckClipboard.setDisabled(True)
        gbNexusModsAPILayout.addWidget(self.nexusCheckClipboard)

        # Output

        gbOutput = QGroupBox('Output Preferences', self)
        mainLayout.addWidget(gbOutput)
        gbOutputLayout = QVBoxLayout(gbOutput)
        self.unhideOutput = QCheckBox('Auto-show output panel', self)
        self.unhideOutput.setChecked(
            settings.value('unhideOutput', 'True') == 'True')
        gbOutputLayout.addWidget(self.unhideOutput)
        self.debugOutput = QCheckBox('Show debug output', self)
        self.debugOutput.setChecked(
            settings.value('debugOutput', 'False') == 'True')
        gbOutputLayout.addWidget(self.debugOutput)

        # Actions

        actionsLayout = QHBoxLayout()
        actionsLayout.setAlignment(Qt.AlignRight)
        self.save = QPushButton('Save', self)
        self.save.clicked.connect(self.saveEvent)
        self.save.setAutoDefault(True)
        self.save.setDefault(True)
        actionsLayout.addWidget(self.save)
        cancel = QPushButton('Cancel', self)
        cancel.clicked.connect(self.cancelEvent)
        actionsLayout.addWidget(cancel)
        mainLayout.addLayout(actionsLayout)

        # Setup

        if not settings.value('gamePath'):
            self.locateGameEvent()
        self.setMinimumSize(QSize(440, 440))
        self.setSizePolicy(QSizePolicy.MinimumExpanding,
                           QSizePolicy.MinimumExpanding)

        self.validGamePath = False
        self.validConfigPath = False
        self.validNexusAPIKey = False
        self.validScriptMergerPath = False

        self.validateGamePath(self.gamePath.text())
        self.validateConfigPath(self.configPath.text())
        self.validateApiKey(self.nexusAPIKey.text())
        self.validateScriptMergerPath(self.scriptMergerPath.text())
        self.updateSaveButton()

        self.finished.connect(
            lambda: self.validateApiKey.cancel())  # type: ignore

    def saveEvent(self) -> None:
        settings = QSettings()
        settings.setValue('settingsWindowGeometry', self.saveGeometry())
        settings.setValue('gamePath', self.gamePath.text())
        settings.setValue('configPath', self.configPath.text())
        settings.setValue('scriptMergerPath', self.scriptMergerPath.text())
        settings.setValue('nexusAPIKey', self.nexusAPIKey.text())
        settings.setValue('nexusGetInfo', str(self.nexusGetInfo.isChecked()))
        settings.setValue('nexusCheckUpdates',
                          str(self.nexusCheckUpdates.isChecked()))
        settings.setValue('nexusCheckClipboard',
                          str(self.nexusCheckClipboard.isChecked()))
        settings.setValue('debugOutput', str(self.debugOutput.isChecked()))
        settings.setValue('unhideOutput', str(self.unhideOutput.isChecked()))
        self.close()

    def cancelEvent(self) -> None:
        self.close()

    def selectGameEvent(self) -> None:
        dialog: QFileDialog = QFileDialog(self, 'Select witcher3.exe', '',
                                          'The Witcher 3 (witcher3.exe)')
        dialog.setOptions(QFileDialog.ReadOnly)
        dialog.setFileMode(QFileDialog.ExistingFile)
        if (dialog.exec_()):
            if dialog.selectedFiles():
                self.gamePath.setText(dialog.selectedFiles()[0])

    def selectConfigEvent(self) -> None:
        dialog: QFileDialog = QFileDialog(self, 'Select config folder', '',
                                          'The Witcher 3')
        dialog.setOptions(QFileDialog.ReadOnly)
        dialog.setFileMode(QFileDialog.Directory)
        if (dialog.exec_()):
            if dialog.selectedFiles():
                self.configPath.setText(dialog.selectedFiles()[0])

    def selectScriptMergerEvent(self) -> None:
        dialog: QFileDialog = QFileDialog(
            self, 'Select WitcherScriptMerger.exe', '',
            'Script Merger (WitcherScriptMerger.exe)')
        dialog.setOptions(QFileDialog.ReadOnly)
        dialog.setFileMode(QFileDialog.ExistingFile)
        if (dialog.exec_()):
            if dialog.selectedFiles():
                self.scriptMergerPath.setText(dialog.selectedFiles()[0])

    def locateGameEvent(self) -> None:
        game = fetcher.findGamePath()
        if game:
            self.gamePath.setText(str(game))
        else:
            self.gamePathInfo.setText('''
                <font color="#888">
                Could not detect The Witcher 3!<br>
                Please make sure the game is installed, or set the path manually.
                </font>''')

    def locateConfigEvent(self) -> None:
        config = fetcher.findConfigPath()
        if config:
            self.configPath.setText(str(config))
        else:
            self.configPathInfo.setText('''
                <font color="#888">
                Could not detect a valid config path!
                Please make sure the The Witcher 3 was started at least once,
                or set the path manually.
                </font>''')

    def locateScriptMergerEvent(self) -> None:
        scriptmerger = findScriptMergerPath()
        if scriptmerger:
            self.scriptMergerPath.setText(str(scriptmerger))
        else:
            self.scriptMergerPathInfo.setText('''
                <font color="#888">
                Could not detect Script Merger! Please make sure Script Merger is running,<br>
                or set the path manually.
                Download Script Merger <a href="https://www.nexusmods.com/witcher3/mods/484">here</a>.
                </font>''')

    def validateGamePath(self, text: str) -> bool:
        # validate game installation path
        if not verifyGamePath(Path(text)):
            self.gamePath.setStyleSheet('''
                *{
                    border: 1px solid #B22222;
                    padding: 1px 0px;
                }
                ''')
            self.gamePathInfo.setText(
                '<font color="#888">Please enter a valid game path.</font>')
            self.validGamePath = False
            self.locateGame.setDisabled(False)
            self.updateSaveButton()
            return False
        else:
            self.gamePath.setStyleSheet('')
            self.gamePathInfo.setText(
                '<font color="#888">Everything looks good!</font>')
            self.validGamePath = True
            self.locateGame.setDisabled(True)
            self.updateSaveButton()
            return True

    def validateConfigPath(self, text: str) -> bool:
        # validate game config path
        if not verifyConfigPath(Path(text)):
            self.configPath.setStyleSheet('''
                *{
                    border: 1px solid #B22222;
                    padding: 1px 0px;
                }
                ''')
            self.configPathInfo.setText('''<font color="#888">
                Please enter a valid config path.
                You need to start the The Witcher 3 at least once
                to generate the necessary user.settings and input.settings files.</font>
                ''')
            self.validConfigPath = False
            self.locateConfig.setDisabled(False)
            self.updateSaveButton()
            return False
        else:
            self.configPath.setStyleSheet('')
            self.configPathInfo.setText(
                '<font color="#888">Everything looks good!</font>')
            self.validConfigPath = True
            self.locateConfig.setDisabled(True)
            self.updateSaveButton()
            return True

    def validateScriptMergerPath(self, text: str) -> bool:
        # validate script merger path
        if not text:
            self.scriptMergerPath.setStyleSheet('')
            self.scriptMergerPathInfo.setText('''
                <font color="#888">Script Merger is used to resolve conflicts between mods \
                by merging scripts and other text files. \
                Download Script Merger <a href="https://www.nexusmods.com/witcher3/mods/484">here</a>.</font>
                ''')
            self.validScriptMergerPath = True
            self.updateSaveButton()
            return True
        if not verifyScriptMergerPath(Path(text)):
            self.scriptMergerPath.setStyleSheet('''
                *{
                    border: 1px solid #B22222;
                    padding: 1px 0px;
                }
                ''')
            self.scriptMergerPathInfo.setText(
                '''<font color="#888">Please enter a valid script merger path.</font>
                ''')
            self.validScriptMergerPath = False
            self.locateScriptMerger.setDisabled(False)
            self.updateSaveButton()
            return False
        else:
            self.scriptMergerPath.setStyleSheet('')
            self.scriptMergerPathInfo.setText(
                '<font color="#888">Everything looks good!</font>')
            self.validScriptMergerPath = True
            self.locateScriptMerger.setDisabled(True)
            self.updateSaveButton()
            return True

    @debounce(200, cancel_running=True)
    async def validateApiKey(self, text: str) -> bool:
        # validate neus mods api key
        self.nexusGetInfo.setDisabled(True)
        self.nexusCheckUpdates.setDisabled(True)
        self.nexusCheckClipboard.setDisabled(True)
        self.nexusAPIKey.setStyleSheet('')
        if not text:
            self.nexusAPIKeyInfo.setText('''
                <font color="#888">The API Key is used to check for mod updates, \
                to get mod details and to download mods. \
                Get your Personal API Key <a href="https://www.nexusmods.com/users/myaccount?tab=api">here</a>.</font>
                ''')
            self.validNexusAPIKey = True
            self.updateSaveButton()
            return True
        self.nexusAPIKeyInfo.setText('🌐')
        try:
            apiUser = await getUserInformation(text)
        except UnauthorizedError:
            self.nexusAPIKey.setStyleSheet('''
                *{
                    border: 1px solid #B22222;
                    padding: 1px 0px;
                }
                ''')
            self.nexusAPIKeyInfo.setText('''
                <font color="#888">Not a valid API Key. \
                Get your Personal API Key <a href="https://www.nexusmods.com/users/myaccount?tab=api">here</a>.</font>
                ''')
            self.validNexusAPIKey = False
            self.updateSaveButton()
            return False
        except (RequestError, ResponseError, Exception) as e:
            self.nexusAPIKey.setStyleSheet('''
                *{
                    border: 1px solid #B22222;
                    padding: 1px 0px;
                }
                ''')
            self.nexusAPIKeyInfo.setText(f'''
                <font color="#888">Could not validate API Key: {str(e) if str(e) else 'Request error'}.</font>
                ''')
            self.validNexusAPIKey = False
            self.updateSaveButton()
            return False
        self.nexusAPIKeyInfo.setText(
            f'<font color="#888">Valid API Key for {apiUser["name"]}!</font>')
        self.validNexusAPIKey = True
        self.nexusGetInfo.setDisabled(False)
        self.nexusCheckUpdates.setDisabled(False)
        self.nexusCheckClipboard.setDisabled(False)
        self.updateSaveButton()
        return True

    def updateSaveButton(self) -> None:
        # TODO: release: disable saving invalid settings
        # self.save.setDisabled(not all((
        #     self.validConfigPath,
        #     self.validGamePath,
        #     self.validNexusAPIKey,
        #     self.validScriptMergerPath,
        # )))  # noqa
        self.save.setDisabled(False)
Пример #5
0
class DownloadWindow(QDialog):
    def __init__(self,
                 parent: Optional[QWidget] = None,
                 url: str = '') -> None:
        super().__init__(parent, )

        if parent:
            self.setWindowTitle('Download Mod')
        else:
            self.setWindowTitle(getTitleString('Download Mod'))
            self.setAttribute(Qt.WA_DeleteOnClose)

        mainLayout = QVBoxLayout(self)
        mainLayout.setContentsMargins(5, 5, 5, 5)

        self.signals = DownloadWindowEvents(self)

        # URL input

        gbUrl = QGroupBox('Mod URL')
        gbUrlLayout = QVBoxLayout()
        gbUrl.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Minimum)

        self.url = QLineEdit()
        self.url.setPlaceholderText(
            'https://www.nexusmods.com/witcher3/mods/...')
        self.url.setText(url)
        self.url.textChanged.connect(lambda: self.validateUrl(self.url.text()))
        gbUrlLayout.addWidget(self.url)

        self.urlInfo = QLabel('🌐')
        self.urlInfo.setContentsMargins(4, 4, 4, 4)
        self.urlInfo.setMinimumHeight(36)
        self.urlInfo.setWordWrap(True)
        gbUrlLayout.addWidget(self.urlInfo)

        gbUrl.setLayout(gbUrlLayout)
        mainLayout.addWidget(gbUrl)

        # File selection

        gbFiles = QGroupBox('Mod Files')
        gbFilesLayout = QVBoxLayout()
        gbFiles.setSizePolicy(QSizePolicy.MinimumExpanding,
                              QSizePolicy.MinimumExpanding)

        self.files = QTableWidget(0, 4)
        self.files.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.files.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel)
        self.files.setContextMenuPolicy(Qt.CustomContextMenu)
        self.files.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.files.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.files.setWordWrap(False)
        self.files.setSortingEnabled(True)
        self.files.setFocusPolicy(Qt.StrongFocus)
        self.files.verticalHeader().hide()
        self.files.setSortingEnabled(True)
        self.files.sortByColumn(2, Qt.DescendingOrder)
        self.files.verticalHeader().setVisible(False)
        self.files.verticalHeader().setDefaultSectionSize(25)
        self.files.horizontalHeader().setHighlightSections(False)
        self.files.horizontalHeader().setStretchLastSection(True)
        self.files.setHorizontalHeaderLabels(
            ['File Name', 'Version', 'Upload Date', 'Description'])
        self.files.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.files.verticalScrollBar().valueChanged.connect(
            lambda: self.files.clearFocus())
        self.files.itemSelectionChanged.connect(lambda: self.validateFiles())
        self.files.setDisabled(True)
        self.files.setStyleSheet('''
            QTableView {
                gridline-color: rgba(255,255,255,1);
            }
            QTableView::item {
                padding: 5px;
                margin: 1px 0;
            }
            QTableView::item:!selected:hover {
                background-color: rgb(217, 235, 249);
                padding: 0;
            }
            ''')
        gbFilesLayout.addWidget(self.files)

        _mouseMoveEvent = self.files.mouseMoveEvent
        self.files.hoverIndexRow = -1

        def mouseMoveEvent(event: QMouseEvent) -> None:
            self.files.hoverIndexRow = self.files.indexAt(event.pos()).row()
            _mouseMoveEvent(event)

        self.files.mouseMoveEvent = mouseMoveEvent  # type: ignore
        self.files.setItemDelegate(ModListItemDelegate(self.files))
        self.files.setMouseTracking(True)

        gbFiles.setLayout(gbFilesLayout)
        mainLayout.addWidget(gbFiles)

        # Actions

        actionsLayout = QHBoxLayout()
        actionsLayout.setAlignment(Qt.AlignRight)
        self.download = QPushButton('Download', self)
        self.download.clicked.connect(lambda: self.downloadEvent())
        self.download.setAutoDefault(True)
        self.download.setDefault(True)
        self.download.setDisabled(True)
        actionsLayout.addWidget(self.download)
        cancel = QPushButton('Cancel', self)
        cancel.clicked.connect(self.cancelEvent)
        actionsLayout.addWidget(cancel)
        mainLayout.addLayout(actionsLayout)

        # Setup

        self.setMinimumSize(QSize(420, 420))
        self.setSizePolicy(QSizePolicy.MinimumExpanding,
                           QSizePolicy.MinimumExpanding)
        self.resize(QSize(720, 420))

        self.finished.connect(
            lambda: self.validateUrl.cancel())  # type: ignore
        self.finished.connect(
            lambda: self.downloadEvent.cancel())  # type: ignore

        self.modId = 0
        self.validateUrl(self.url.text())

    def cancelEvent(self) -> None:
        self.close()

    @debounce(200, cancel_running=True)
    async def validateUrl(self, url: str) -> bool:
        self.download.setDisabled(True)
        self.files.setDisabled(True)
        self.files.clearSelection()
        self.files.clearFocus()
        self.files.clearContents()
        self.files.setRowCount(0)
        self.files.setSortingEnabled(False)
        self.url.setStyleSheet('')
        self.modId = 0
        if not url:
            self.urlInfo.setText('''
                <font color="#888">Please enter a valid mod url.</font>
                ''')
            return False
        modId = getModId(url)
        if not modId:
            self.files.setDisabled(True)
            self.url.setStyleSheet('''
                *{
                    border: 1px solid #B22222;
                    padding: 1px 0px;
                }
                ''')
            self.urlInfo.setText('''
                <font color="#888">Please enter a valid mod url.</font>
                ''')
            return False
        self.urlInfo.setText('🌐')
        try:
            filesResponse = await getModFiles(modId)
        except (RequestError, ResponseError, Exception) as e:
            self.url.setStyleSheet('''
                *{
                    border: 1px solid #B22222;
                    padding: 1px 0px;
                }
                ''')
            self.urlInfo.setText(f'''
                <font color="#888">Could not get mod files: {e}.</font>
                ''')
            return False
        try:
            files = filesResponse['files']
            if not len(files):
                self.urlInfo.setText(f'''
                    <font color="#888">Mod "{modId}" has no files!</font>
                    ''')
                return False

            self.files.setRowCount(len(files))
            for i in range(len(files)):
                file = files[i]
                fileid = int(file['file_id'])
                name = str(file['name'])
                version = str(file['version'])
                _uploadtime = dateparser.parse(file['uploaded_time'])
                uploadtime = _uploadtime.astimezone(tz=None).strftime(
                    '%Y-%m-%d %H:%M:%S') if _uploadtime else '?'
                description = html.unescape(str(file['description']))
                nameItem = QTableWidgetItem(name)
                nameItem.setToolTip(name)
                nameItem.setData(Qt.UserRole, fileid)
                self.files.setItem(i, 0, nameItem)
                versionItem = QTableWidgetItem(version)
                versionItem.setToolTip(version)
                self.files.setItem(i, 1, versionItem)
                uploadtimeItem = QTableWidgetItem(uploadtime)
                uploadtimeItem.setToolTip(uploadtime)
                self.files.setItem(i, 2, uploadtimeItem)
                descriptionItem = QTableWidgetItem(description)
                descriptionItem.setToolTip(description)
                self.files.setItem(i, 3, descriptionItem)
        except KeyError as e:
            logger.exception(
                f'Could not find key "{str(e)}" in mod files response')
            self.urlInfo.setText(f'''
                <font color="#888">Could not find key "{str(e)}" in mod files response.</font>
                ''')
            return False

        self.urlInfo.setText(f'''
            <font color="#888">Found {len(files)} available files.</font>
            ''')
        self.files.resizeColumnsToContents()
        self.files.setDisabled(False)
        self.files.setSortingEnabled(True)
        self.modId = modId
        return True

    def validateFiles(self) -> bool:
        selection = self.files.selectionModel().selectedRows()
        if len(selection) > 0:
            self.download.setText(f'Download {len(selection)} mods')
            self.download.setDisabled(False)
            return True
        return False

    @debounce(25, cancel_running=True)
    async def downloadEvent(self) -> None:
        self.download.setDisabled(True)
        self.url.setDisabled(True)
        selection = self.files.selectionModel().selectedRows()
        files = [
            self.files.item(index.row(), 0).data(Qt.UserRole)
            for index in selection
        ]
        self.files.setDisabled(True)
        try:
            urls = await asyncio.gather(
                *[getModFileUrls(self.modId, file) for file in files],
                loop=asyncio.get_running_loop())
        except (RequestError, ResponseError, Exception) as e:
            self.url.setStyleSheet('''
                *{
                    border: 1px solid #B22222;
                    padding: 1px 0px;
                }
                ''')
            self.urlInfo.setText(f'''
                <font color="#888">Could not download mod files: {e}.</font>
                ''')
            return
        try:
            self.signals.download.emit([url[0]['URI'] for url in urls])
        except KeyError as e:
            logger.exception(
                f'Could not find key "{str(e)}" in file download response')
            self.urlInfo.setText(f'''
                <font color="#888">Could not find key "{str(e)}" in file download response.</font>
                ''')
            return
        self.close()