class DirectoryWidget(QToolButton): Sg_double_clicked = Signal(str) def __init__(self): super(DirectoryWidget, self).__init__() self.timer = QTimer() self.timer.setSingleShot(True) self.clicked.connect(self.Sl_check_double_click) self.setAccessibleName('Directory') self.setIcon(QIcon(resource_path("icons/Cartella.png"))) self.setIconSize(QSize(45, 45)) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) @Slot() def Sl_check_double_click(self): success = False if self.timer.isActive(): time = self.timer.remainingTime() if time > 0: self.double_clicked_action() success = True self.timer.stop() if time <= 0: self.timer.start(250) if not self.timer.isActive() and not success: self.timer.start(250) def double_clicked_action(self) -> None: pass
class FileWidget(QToolButton): Sg_double_clicked = Signal() def __init__(self, file: File): super(FileWidget, self).__init__() self.timer = QTimer() self.timer.setSingleShot(True) self.clicked.connect(self.check_double_click) self.Sg_double_clicked.connect(self.Sl_on_double_click) self.setAccessibleName('File') self.name = file.get_name() self.creation_date = file.get_creation_date() self.last_modified_date = file.get_last_modified_date() self.extension = self.get_extension() self.set_icon() self.setText(self.name) def get_extension(self) -> str: if self.name.find('.') != -1: e = self.name.split(".") return e[-1] else: return "no" def set_icon(self): self.setIconSize(QSize(45, 45)) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) if self.extension in ["txt", "xml", "json", "docx", "xlsx"]: self.setIcon(QIcon(resource_path('icons/Txt.png'))) elif self.extension in ["mp4", "avi", "mpeg", "wmv"]: self.setIcon(QIcon(resource_path('icons/Video.png'))) elif self.extension in ["jpg", "png", "gif"]: self.setIcon(QIcon(resource_path('icons/Immagine.png'))) elif self.extension in ["mp3", "wav", "ogg"]: self.setIcon(QIcon(resource_path('icons/Audio.png'))) else: self.setIcon(QIcon(resource_path('icons/DocGenerico.png'))) def check_double_click(self): if self.timer.isActive(): time = self.timer.remainingTime() if time > 0: self.Sg_double_clicked.emit() self.timer.stop() if time <= 0: self.timer.start(250) if self.timer.isActive() is False: self.timer.start(250) @Slot() def Sl_on_double_click(self): pass
class RenderWindow(QWindow): def __init__(self, format): super(RenderWindow, self).__init__() self.setSurfaceType(QWindow.OpenGLSurface) self.setFormat(format) self.context = QOpenGLContext(self) self.context.setFormat(self.requestedFormat()) if not self.context.create(): raise Exception("Unable to create GL context") self.program = None self.timer = None self.angle = 0 def initGl(self): self.program = QOpenGLShaderProgram(self) self.vao = QOpenGLVertexArrayObject() self.vbo = QOpenGLBuffer() format = self.context.format() useNewStyleShader = format.profile() == QSurfaceFormat.CoreProfile # Try to handle 3.0 & 3.1 that do not have the core/compatibility profile # concept 3.2+ has. This may still fail since version 150 (3.2) is # specified in the sources but it's worth a try. if (format.renderableType() == QSurfaceFormat.OpenGL and format.majorVersion() == 3 and format.minorVersion() <= 1): useNewStyleShader = not format.testOption( QSurfaceFormat.DeprecatedFunctions) vertexShader = vertexShaderSource if useNewStyleShader else vertexShaderSource110 fragmentShader = fragmentShaderSource if useNewStyleShader else fragmentShaderSource110 if not self.program.addShaderFromSourceCode(QOpenGLShader.Vertex, vertexShader): raise Exception("Vertex shader could not be added: {} ({})".format( self.program.log(), vertexShader)) if not self.program.addShaderFromSourceCode(QOpenGLShader.Fragment, fragmentShader): raise Exception( "Fragment shader could not be added: {} ({})".format( self.program.log(), fragmentShader)) if not self.program.link(): raise Exception("Could not link shaders: {}".format( self.program.log())) self.posAttr = self.program.attributeLocation("posAttr") self.colAttr = self.program.attributeLocation("colAttr") self.matrixUniform = self.program.uniformLocation("matrix") self.vbo.create() self.vbo.bind() self.verticesData = vertices.tobytes() self.colorsData = colors.tobytes() verticesSize = 4 * vertices.size colorsSize = 4 * colors.size self.vbo.allocate(VoidPtr(self.verticesData), verticesSize + colorsSize) self.vbo.write(verticesSize, VoidPtr(self.colorsData), colorsSize) self.vbo.release() vaoBinder = QOpenGLVertexArrayObject.Binder(self.vao) if self.vao.isCreated(): # have VAO support, use it self.setupVertexAttribs() def setupVertexAttribs(self): self.vbo.bind() self.program.setAttributeBuffer(self.posAttr, GL.GL_FLOAT, 0, 2) self.program.setAttributeBuffer(self.colAttr, GL.GL_FLOAT, 4 * vertices.size, 3) self.program.enableAttributeArray(self.posAttr) self.program.enableAttributeArray(self.colAttr) self.vbo.release() def exposeEvent(self, event): if self.isExposed(): self.render() if self.timer is None: self.timer = QTimer(self) self.timer.timeout.connect(self.slotTimer) if not self.timer.isActive(): self.timer.start(10) else: if self.timer and self.timer.isActive(): self.timer.stop() def render(self): if not self.context.makeCurrent(self): raise Exception("makeCurrent() failed") functions = self.context.functions() if self.program is None: functions.glEnable(GL.GL_DEPTH_TEST) functions.glClearColor(0, 0, 0, 1) self.initGl() retinaScale = self.devicePixelRatio() functions.glViewport(0, 0, self.width() * retinaScale, self.height() * retinaScale) functions.glClear(GL.GL_COLOR_BUFFER_BIT | GL.GL_DEPTH_BUFFER_BIT) self.program.bind() matrix = QMatrix4x4() matrix.perspective(60, 4 / 3, 0.1, 100) matrix.translate(0, 0, -2) matrix.rotate(self.angle, 0, 1, 0) self.program.setUniformValue(self.matrixUniform, matrix) if self.vao.isCreated(): self.vao.bind() else: # no VAO support, set the vertex attribute arrays now self.setupVertexAttribs() functions.glDrawArrays(GL.GL_TRIANGLES, 0, 3) self.vao.release() self.program.release() # swapInterval is 1 by default which means that swapBuffers() will (hopefully) block # and wait for vsync. self.context.swapBuffers(self) self.context.doneCurrent() def slotTimer(self): self.render() self.angle += 1 def glInfo(self): if not self.context.makeCurrent(self): raise Exception("makeCurrent() failed") functions = self.context.functions() text = """Vendor: {}\nRenderer: {}\nVersion: {}\nShading language: {} \nContext Format: {}\n\nSurface Format: {}""".format( functions.glGetString(GL.GL_VENDOR), functions.glGetString(GL.GL_RENDERER), functions.glGetString(GL.GL_VERSION), functions.glGetString(GL.GL_SHADING_LANGUAGE_VERSION), print_surface_format(self.context.format()), print_surface_format(self.format())) self.context.doneCurrent() return text
class ModList(QTableView): def __init__(self, parent: QWidget, model: Model) -> None: super().__init__(parent) settings = QSettings() self.hoverIndexRow = -1 self.modmodel = model self.installLock = asyncio.Lock() self.setMouseTracking(True) self.setSelectionMode(QAbstractItemView.ExtendedSelection) self.setSelectionBehavior(QAbstractItemView.SelectRows) self.setWordWrap(False) self.setSortingEnabled(True) self.setFocusPolicy(Qt.StrongFocus) self.setAcceptDrops(True) self.setEditTriggers(QTableView.EditKeyPressed | QTableView.DoubleClicked) self.setShowGrid(False) self.setStyleSheet(''' QTableView { gridline-color: rgba(255,255,255,1); } QTableView::item:!selected:hover { background-color: rgb(217, 235, 249); } ''') self.setVerticalScrollMode(QAbstractItemView.ScrollPerPixel) self.setHorizontalScrollMode(QAbstractItemView.ScrollPerPixel) self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.showContextMenu) self.verticalHeader().hide() self.verticalHeader().setVisible(False) self.setSectionSize(settings.value('compactMode', 'False') == 'True') self.setCornerButtonEnabled(False) self.horizontalHeader().setHighlightSections(False) self.horizontalHeader().setStretchLastSection(True) self.horizontalHeader().setSectionsMovable(True) self.listmodel = ModListModel(self, model) self.filtermodel = ModListFilterModel(self, self.listmodel) self.setModel(self.filtermodel) self.setItemDelegate(ModListItemDelegate(self)) self.setSelectionModel(ModListSelectionModel(self, self.filtermodel)) if len(model): self.modCountLastUpdate = len(model) self.resizeColumnsToContents() else: self.modCountLastUpdate = -1 if settings.value('modlistHorizontalHeaderState'): self.horizontalHeader().restoreState( settings.value('modlistHorizontalHeaderState')) # type: ignore self.horizontalHeader().sectionMoved.connect( lambda: self.headerChangedEvent()) self.horizontalHeader().sectionResized.connect( lambda: self.headerChangedEvent()) self.setFocus() self.sortByColumn(3, Qt.AscendingOrder, False) self.sortByColumn(2, Qt.AscendingOrder, False) self.sortByColumn(1, Qt.AscendingOrder, False) if settings.value('modlistSortColumn') is not None and \ settings.value('modlistSortOrder') is not None: try: self.sortByColumn( cast(int, settings.value('modlistSortColumn', 1, int)), Qt.DescendingOrder if cast(int, settings.value('modlistSortOrder', 1, int)) else Qt.AscendingOrder, False) except Exception as e: logger.exception(f'could not restore sort order: {e}') self.horizontalHeader().sortIndicatorChanged.connect(self.sortByColumn) self.doubleClicked.connect(self.doubleClickEvent) model.updateCallbacks.append(self.modelUpdateEvent) # setup viewport caching to counter slow resizing with many table elements self.resizeTimer = QTimer(self) self.resizeTimer.setSingleShot(True) self.resizeTimer.setInterval(250) self.resizeTimer.timeout.connect(lambda: [ self.resizeTimer.stop(), self.viewport().repaint(), ]) self.viewportCache = QPixmap() self.viewportCacheSize = QSize(0, 0) # TODO: enhancement: offer option to read readme and other additional text files def setSectionSize(self, compact: bool) -> None: if compact: self.verticalHeader().setDefaultSectionSize(25) else: self.verticalHeader().setDefaultSectionSize(30) @debounce(200) async def headerChangedEvent(self) -> None: settings = QSettings() state = self.horizontalHeader().saveState() # call later to work around pyqt5 StopIteration exception asyncio.get_running_loop().call_later( 25 / 1000.0, lambda: settings.setValue('modlistHorizontalHeaderState', state)) def modelUpdateEvent(self, model: Model) -> None: if not self.modCountLastUpdate and len(self.modmodel): # if list was empty before, auto resize columns self.resizeColumnsToContents() self.modCountLastUpdate = len(self.modmodel) def mouseMoveEvent(self, event: QMouseEvent) -> None: self.hoverIndexRow = self.indexAt(event.pos()).row() return super().mouseMoveEvent(event) def wheelEvent(self, event: QWheelEvent) -> None: result = super().wheelEvent(event) # repaint previously hovered row on scroll avoid repaint artifacts index = self.model().index(self.hoverIndexRow, 0) self.hoverIndexRow = self.indexAt(event.position().toPoint()).row() rect = self.visualRect(index) rect.setLeft(0) rect.setRight(self.viewport().width()) self.viewport().repaint(rect) return result def leaveEvent(self, event: QEvent) -> None: index = self.model().index(self.hoverIndexRow, 0) # unset row hover state and repaint previously hovered row self.hoverIndexRow = -1 rect = self.visualRect(index) rect.setLeft(0) rect.setRight(self.viewport().width()) self.viewport().repaint(rect) return super().leaveEvent(event) def doubleClickEvent(self, index: QModelIndex) -> None: if self.filtermodel.mapToSource(index).column() == 0: mod = self.modmodel[self.filtermodel.mapToSource(index).row()] if mod.enabled: asyncio.create_task(self.modmodel.disable(mod)) else: asyncio.create_task(self.modmodel.enable(mod)) def resizeEvent(self, event: QResizeEvent) -> None: super().resizeEvent(event) if not self.resizeTimer.isActive( ) and event.size() != self.viewportCacheSize: self.viewportCacheSize = event.size() self.viewportCache = self.viewport().grab() self.resizeTimer.start() def paintEvent(self, event: QPaintEvent) -> None: if self.resizeTimer.isActive(): painter = QPainter(self.viewport()) painter.drawPixmap(0, 0, self.viewportCache) else: super().paintEvent(event) def selectionChanged(self, selected: QItemSelection, deselected: QItemSelection) -> None: super().selectionChanged(selected, deselected) def eventFilter(self, obj: QObject, event: QEvent) -> bool: return super().eventFilter(obj, event) def sortByColumn(self, col: int, order: Qt.SortOrder, save: bool = True) -> None: # type: ignore if save and col is not None and order is not None: settings = QSettings() settings.setValue('modlistSortColumn', col) settings.setValue('modlistSortOrder', 0 if order == Qt.AscendingOrder else 1) super().sortByColumn(col, order) def showContextMenu(self, pos: QPoint) -> None: mods = self.getSelectedMods() if not mods: return menu = QMenu(self) actionOpen = menu.addAction('&Open Directory') actionOpen.setIcon( QIcon(str(getRuntimePath('resources/icons/open-folder.ico')))) actionOpen.triggered.connect(lambda: [ util.openDirectory(self.modmodel.getModPath(mod)) # type: ignore for mod in mods ]) menu.addSeparator() actionEnable = menu.addAction('&Enable') actionEnable.triggered.connect( lambda: [asyncio.create_task(self.enableSelectedMods(True))]) actionEnable.setEnabled(not all(mod.enabled for mod in mods)) actionDisable = menu.addAction('&Disable') actionDisable.triggered.connect( lambda: [asyncio.create_task(self.enableSelectedMods(False))]) actionDisable.setEnabled(not all(not mod.enabled for mod in mods)) menu.addSeparator() actionUninstall = menu.addAction('&Uninstall') actionUninstall.triggered.connect( lambda: [asyncio.create_task(self.deleteSelectedMods())]) menu.addSeparator() actionOpenNexus = menu.addAction('Open &Nexus Mods page') actionOpenNexus.setIcon( QIcon(str(getRuntimePath('resources/icons/browse.ico')))) actionOpenNexus.triggered.connect(lambda: [ QDesktopServices.openUrl( QUrl(f'https://www.nexusmods.com/witcher3/mods/{modid}')) for modid in {mod.modid for mod in mods if mod.modid > 0} ]) actionOpenNexus.setEnabled(not all(mod.modid <= 0 for mod in mods)) menu.popup(self.viewport().mapToGlobal(pos)) def selectRowChecked(self, row: int) -> None: nums: int = self.filtermodel.rowCount() if row < nums and row >= 0: self.selectRow(row) elif nums > 0: self.selectRow(nums - 1) def getSelectedMods(self) -> List[Mod]: return [ self.modmodel[self.filtermodel.mapToSource(index).row()] for index in self.selectionModel().selectedRows() ] async def enableSelectedMods(self, enable: bool = True) -> None: if not self.selectionModel().hasSelection(): return mods = self.getSelectedMods() self.setDisabled(True) for mod in mods: try: if enable: await self.modmodel.enable(mod) else: await self.modmodel.disable(mod) except Exception as e: logger.bind(name=mod.filename).exception( f'Could not enable/disable mod: {e}') self.setDisabled(False) self.setFocus() async def deleteSelectedMods(self) -> None: if not self.selectionModel().hasSelection(): return self.setDisabled(True) mods = self.getSelectedMods() # TODO: incomplete: ask if selected mods should really be removed inds = self.selectedIndexes() self.selectionModel().clear() for mod in mods: try: await self.modmodel.remove(mod) except Exception as e: logger.bind( name=mod.filename).exception(f'Could not delete mod: {e}') asyncio.get_running_loop().call_later( 100 / 1000.0, partial(self.selectRowChecked, inds[0].row())) self.setDisabled(False) self.setFocus() async def updateModDetails(self, mod: Mod) -> bool: logger.bind(name=mod.filename, dots=True).debug('Requesting details for mod') if not mod.md5hash: logger.bind(name=mod.filename).warning( 'Could not get details for mod not installed from archive') return False try: details = await getModInformation(mod.md5hash) except Exception as e: logger.bind(name=mod.filename).warning(f'{e}') return False try: package = str(details[0]['mod']['name']) summary = str(details[0]['mod']['summary']) modid = int(details[0]['mod']['mod_id']) category = int(details[0]['mod']['category_id']) version = str(details[0]['file_details']['version']) fileid = int(details[0]['file_details']['file_id']) uploadname = str(details[0]['file_details']['name']) uploadtime = str(details[0]['file_details']['uploaded_time']) mod.package = package mod.summary = summary mod.modid = modid mod.category = getCategoryName(category) mod.version = version mod.fileid = fileid mod.uploadname = uploadname uploaddate = dateparser.parse(uploadtime) if uploaddate: mod.uploaddate = uploaddate.astimezone(tz=timezone.utc) else: logger.bind(name=mod.filename).debug( f'Could not parse date {uploadtime} in mod information response' ) except KeyError as e: logger.bind(name=mod.filename).exception( f'Could not find key "{str(e)}" in mod information response') return False try: await self.modmodel.update(mod) except Exception as e: logger.bind( name=mod.filename).exception(f'Could not update mod: {e}') return False return True async def updateSelectedModsDetails(self) -> None: if not self.selectionModel().hasSelection(): return self.setDisabled(True) updatetime = datetime.now(tz=timezone.utc) mods = self.getSelectedMods() logger.bind( newline=True, output=False).debug(f'Requesting details for {len(mods)} mods') results = await asyncio.gather( *[self.updateModDetails(mod) for mod in mods], loop=asyncio.get_running_loop(), return_exceptions=True) successes = sum(results) errors = len(results) - successes message = 'Updated details for {0} mods{1}'.format( successes, f' ({errors} errors)' if errors else '') if errors: logger.warning(message) else: logger.success(message) self.modmodel.setLastUpdateTime(updatetime) self.setDisabled(False) self.setFocus() async def changeSelectedModsPriority(self, delta: int) -> None: mods = self.getSelectedMods() await asyncio.gather(*[ self.modmodel.setPriority( mod, max(-1, min(9999, int(mod.priority + delta)))) for mod in mods if mod.datatype in ( 'mod', 'udf', ) ], loop=asyncio.get_running_loop()) self.modmodel.setLastUpdateTime(datetime.now(tz=timezone.utc)) def keyPressEvent(self, event: QKeyEvent) -> None: if event.key() == Qt.Key_Escape: self.selectionModel().clear() elif event.matches(QKeySequence.Delete): asyncio.create_task(self.deleteSelectedMods()) elif event.modifiers( ) & Qt.ControlModifier == Qt.ControlModifier and event.key( ) == Qt.Key_Up: asyncio.create_task(self.changeSelectedModsPriority(1)) elif event.modifiers( ) & Qt.ControlModifier == Qt.ControlModifier and event.key( ) == Qt.Key_Down: asyncio.create_task(self.changeSelectedModsPriority(-1)) elif event.modifiers( ) & Qt.ControlModifier == Qt.ControlModifier and event.key( ) == Qt.Key_P: index = self.selectionModel().selectedRows()[0] index = index.sibling(index.row(), 5) if index.flags() & Qt.ItemIsEditable: self.setCurrentIndex(index) self.edit(index) else: super().keyPressEvent(event) def setFilter(self, search: str) -> None: self.filtermodel.setFilterRegularExpression( QRegularExpression(search, QRegularExpression.CaseInsensitiveOption)) async def checkInstallFromURLs(self, paths: List[Union[str, QUrl]], local: bool = True, web: bool = True) -> None: await self.installLock.acquire() installed = 0 errors = 0 installtime = datetime.now(tz=timezone.utc) # remove duplicate paths paths = list(set(paths)) logger.bind(newline=True, output=False).debug('Starting install from URLs') try: results = await asyncio.gather(*[ self.installFromURL(path, local, web, installtime) for path in paths ], loop=asyncio.get_running_loop()) for result in results: installed += result[0] errors += result[1] except Exception as e: # we should never land here, but don't lock up the UI if it happens logger.exception(str(e)) errors += 1 if installed > 0 or errors > 0: log = logger.bind(modlist=bool(installed)) message = 'Installed {0} mods{1}'.format( installed, f' ({errors} errors)' if errors else '') if installed > 0 and errors > 0: log.warning(message) elif installed > 0: log.success(message) else: log.error(message) self.setDisabled(False) self.setFocus() self.installLock.release() async def installFromURL( self, path: Union[str, QUrl], local: bool = True, web: bool = True, installtime: Optional[datetime] = None) -> Tuple[int, int]: installed = 0 errors = 0 if not installtime: installtime = datetime.now(tz=timezone.utc) if isinstance(path, QUrl): path = path.toString() if web and isValidModDownloadUrl(path): self.setDisabled(True) logger.bind(dots=True, path=path).info(f'Installing mods from') i, e = await self.installFromFileDownload(path, installtime) installed += i errors += e elif local and isValidFileUrl(path): self.setDisabled(True) path = QUrl(path) logger.bind(dots=True, path=Path( path.toLocalFile())).info(f'Installing mods from') i, e = await self.installFromFile(Path(path.toLocalFile()), installtime) installed += i errors += e else: logger.bind(path=path).error('Could not install mods from') return installed, errors async def installFromFileDownload( self, url: str, installtime: Optional[datetime] = None) -> Tuple[int, int]: installed = 0 errors = 0 if not installtime: installtime = datetime.now(tz=timezone.utc) try: target = Path(urlparse(url).path) filename = re.sub(r'[^\w\-_\. ]', '_', unquote(target.name)) target = Path(tempfile.gettempdir()).joinpath( 'w3modmanager/download').joinpath(f'{filename}') except ValueError: logger.bind(name=url).exception('Wrong request URL') return 0, 1 try: target.parent.mkdir(parents=True, exist_ok=True) logger.bind(name=url).info('Starting to download file') await downloadFile(url, target) installed, errors = await self.installFromFile(target, installtime) except (RequestError, ResponseError, Exception) as e: logger.bind(name=url).exception(f'Failed to download file: {e}') return 0, 1 except Exception as e: logger.exception(str(e)) return 0, 1 finally: if target.is_file(): target.unlink() return installed, errors async def installFromFile( self, path: Path, installtime: Optional[datetime] = None) -> Tuple[int, int]: originalpath = path installed = 0 errors = 0 archive = path.is_file() source = None md5hash = '' details = None detailsrequest: Optional[asyncio.Task] = None if not installtime: installtime = datetime.now(tz=timezone.utc) try: if archive: # unpack archive, set source and request details md5hash = getMD5Hash(path) source = path settings = QSettings() if settings.value('nexusGetInfo', 'False') == 'True': logger.bind( path=str(path), dots=True).debug('Requesting details for archive') detailsrequest = asyncio.create_task( getModInformation(md5hash)) logger.bind(path=str(path), dots=True).debug('Unpacking archive') path = await extractMod(source) # validate and read mod valid, exhausted = containsValidMod(path, searchlimit=8) if not valid: if not exhausted and self.showContinueSearchDialog( searchlimit=8): if not containsValidMod(path): raise InvalidPathError(path, 'Invalid mod') elif not exhausted: raise InvalidPathError(path, 'Stopped searching for mod') else: raise InvalidPathError(path, 'Invalid mod') mods = await Mod.fromDirectory(path, searchCommonRoot=not archive) installedMods = [] # update mod details and add mods to the model for mod in mods: mod.md5hash = md5hash try: # TODO: incomplete: check if mod is installed, ask if replace await self.modmodel.add(mod) installedMods.append(mod) installed += 1 except ModExistsError: logger.bind(path=source if source else mod.source, name=mod.filename).error(f'Mod exists') errors += 1 continue # wait for details response if requested if detailsrequest: try: details = await detailsrequest except (RequestError, ResponseError, Exception) as e: logger.warning( f'Could not get information for {source.name if source else path.name}: {e}' ) # update mod with additional information if source or details: for mod in installedMods: if source: # set source if it differs from the scan directory, e.g. an archive mod.source = source if details: # set additional details if requested and available try: package = str(details[0]['mod']['name']) summary = str(details[0]['mod']['summary']) modid = int(details[0]['mod']['mod_id']) category = int(details[0]['mod']['category_id']) version = str( details[0]['file_details']['version']) fileid = int(details[0]['file_details']['file_id']) uploadname = str( details[0]['file_details']['name']) uploadtime = str( details[0]['file_details']['uploaded_time']) mod.package = package mod.summary = summary mod.modid = modid mod.category = getCategoryName(category) mod.version = version mod.fileid = fileid mod.uploadname = uploadname uploaddate = dateparser.parse(uploadtime) if uploaddate: mod.uploaddate = uploaddate.astimezone( tz=timezone.utc) else: logger.bind(name=mod.filename).debug( f'Could not parse date {uploadtime} in mod information response' ) except KeyError as e: logger.bind(name=mod.filename).exception( f'Could not find key "{str(e)}" in mod information response' ) try: await self.modmodel.update(mod) except Exception: logger.bind(name=mod.filename).warning( 'Could not update mod details') except ModelError as e: logger.bind(path=e.path).error(e.message) errors += 1 except InvalidPathError as e: # TODO: enhancement: better install error message logger.bind(path=e.path).error(e.message) errors += 1 except FileNotFoundError as e: logger.bind( path=e.filename).error(e.strerror if e.strerror else str(e)) errors += 1 except OSError as e: logger.bind( path=e.filename).error(e.strerror if e.strerror else str(e)) errors += 1 except Exception as e: logger.exception(str(e)) errors += 1 finally: if detailsrequest and not detailsrequest.done(): detailsrequest.cancel() if archive and not path == originalpath: try: util.removeDirectory(path) except Exception: logger.bind(path=path).warning( 'Could not remove temporary directory') self.modmodel.setLastUpdateTime(installtime) self.repaint() return installed, errors def showContinueSearchDialog(self, searchlimit: int) -> bool: messagebox = QMessageBox(self) messagebox.setWindowTitle('Unusual search depth') messagebox.setText(f''' <p>No mod detected after searching through {searchlimit} directories.</p> <p>Are you sure this is a valid mod?</p> ''') messagebox.setTextFormat(Qt.RichText) messagebox.setStandardButtons(QMessageBox.Cancel) yes: QPushButton = QPushButton(' Yes, continue searching ', messagebox) yes.setAutoDefault(True) yes.setDefault(True) messagebox.addButton(yes, QMessageBox.YesRole) messagebox.exec_() return messagebox.clickedButton() == yes def dropEvent(self, event: QDropEvent) -> None: event.accept() self.setDisabled(True) self.repaint() asyncio.create_task(self.checkInstallFromURLs(event.mimeData().urls())) def dragEnterEvent(self, event: QDragEnterEvent) -> None: self.setDisabled(True) self.repaint() urls = event.mimeData().urls() if not urls: self.setDisabled(False) self.setFocus() event.ignore() return for url in urls: try: parse = urlparse(url.toString()) if parse.scheme not in ['file']: self.setDisabled(False) event.ignore() return filepath = Path(url.toLocalFile()) if isArchive(filepath) or containsValidMod(filepath, searchlimit=8)[0]: self.setDisabled(False) event.accept() return except Exception as e: logger.debug(str(e)) self.setDisabled(False) self.setFocus() event.ignore() def dragMoveEvent(self, event: QDragMoveEvent) -> None: event.accept() def dragLeaveEvent(self, event: QDragLeaveEvent) -> None: event.accept()
class MainWindow(QMainWindow): """Voice Changer main window.""" def __init__(self, parent=None): super(MainWindow, self).__init__() self.statusBar().showMessage("Move Dial to Deform Microphone Voice !.") self.setWindowTitle(__doc__) self.setMinimumSize(240, 240) self.setMaximumSize(480, 480) self.resize(self.minimumSize()) self.setWindowIcon(QIcon.fromTheme("audio-input-microphone")) self.tray = QSystemTrayIcon(self) self.center() QShortcut("Ctrl+q", self, activated=lambda: self.close()) self.menuBar().addMenu("&File").addAction("Quit", lambda: exit()) self.menuBar().addMenu("Sound").addAction( "STOP !", lambda: call('killall rec', shell=True)) windowMenu = self.menuBar().addMenu("&Window") windowMenu.addAction("Hide", lambda: self.hide()) windowMenu.addAction("Minimize", lambda: self.showMinimized()) windowMenu.addAction("Maximize", lambda: self.showMaximized()) windowMenu.addAction("Restore", lambda: self.showNormal()) windowMenu.addAction("FullScreen", lambda: self.showFullScreen()) windowMenu.addAction("Center", lambda: self.center()) windowMenu.addAction("Top-Left", lambda: self.move(0, 0)) windowMenu.addAction("To Mouse", lambda: self.move_to_mouse_position()) # widgets group0 = QGroupBox("Voice Deformation") self.setCentralWidget(group0) self.process = QProcess(self) self.process.error.connect( lambda: self.statusBar().showMessage("Info: Process Killed", 5000)) self.control = QDial() self.control.setRange(-10, 20) self.control.setSingleStep(5) self.control.setValue(0) self.control.setCursor(QCursor(Qt.OpenHandCursor)) self.control.sliderPressed.connect( lambda: self.control.setCursor(QCursor(Qt.ClosedHandCursor))) self.control.sliderReleased.connect( lambda: self.control.setCursor(QCursor(Qt.OpenHandCursor))) self.control.valueChanged.connect( lambda: self.control.setToolTip(f"<b>{self.control.value()}")) self.control.valueChanged.connect(lambda: self.statusBar().showMessage( f"Voice deformation: {self.control.value()}", 5000)) self.control.valueChanged.connect(self.run) self.control.valueChanged.connect(lambda: self.process.kill()) # Graphic effect self.glow = QGraphicsDropShadowEffect(self) self.glow.setOffset(0) self.glow.setBlurRadius(99) self.glow.setColor(QColor(99, 255, 255)) self.control.setGraphicsEffect(self.glow) self.glow.setEnabled(False) # Timer to start self.slider_timer = QTimer(self) self.slider_timer.setSingleShot(True) self.slider_timer.timeout.connect(self.on_slider_timer_timeout) # an icon and set focus QLabel(self.control).setPixmap( QIcon.fromTheme("audio-input-microphone").pixmap(32)) self.control.setFocus() QVBoxLayout(group0).addWidget(self.control) self.menu = QMenu(__doc__) self.menu.addAction(__doc__).setDisabled(True) self.menu.setIcon(self.windowIcon()) self.menu.addSeparator() self.menu.addAction( "Show / Hide", lambda: self.hide() if self.isVisible() else self.showNormal()) self.menu.addAction("STOP !", lambda: call('killall rec', shell=True)) self.menu.addSeparator() self.menu.addAction("Quit", lambda: exit()) self.tray.setContextMenu(self.menu) self.make_trayicon() def run(self): """Run/Stop the QTimer.""" if self.slider_timer.isActive(): self.slider_timer.stop() self.glow.setEnabled(True) call('killall rec ; killall play', shell=True) self.slider_timer.start(3000) def on_slider_timer_timeout(self): """Run subprocess to deform voice.""" self.glow.setEnabled(False) value = int(self.control.value()) * 100 command = f'play -q -V0 "|rec -q -V0 -n -d -R riaa bend pitch {value} "' print(f"Voice Deformation Value: {value}") print(f"Voice Deformation Command: {command}") self.process.start(command) if self.isVisible(): self.statusBar().showMessage("Minimizing to System TrayIcon", 3000) print("Minimizing Main Window to System TrayIcon now...") sleep(3) self.hide() def center(self): """Center Window on the Current Screen,with Multi-Monitor support.""" window_geometry = self.frameGeometry() mousepointer_position = QApplication.desktop().cursor().pos() screen = QApplication.desktop().screenNumber(mousepointer_position) centerPoint = QApplication.desktop().screenGeometry(screen).center() window_geometry.moveCenter(centerPoint) self.move(window_geometry.topLeft()) def move_to_mouse_position(self): """Center the Window on the Current Mouse position.""" window_geometry = self.frameGeometry() window_geometry.moveCenter(QApplication.desktop().cursor().pos()) self.move(window_geometry.topLeft()) def make_trayicon(self): """Make a Tray Icon.""" if self.windowIcon() and __doc__: self.tray.setIcon(self.windowIcon()) self.tray.setToolTip(__doc__) self.tray.activated.connect( lambda: self.hide() if self.isVisible() else self.showNormal()) return self.tray.show()