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()
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
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()}' )
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)
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()