class MainWidget(QWidget): def __init__(self, parent: QWidget, model: Model) -> None: super().__init__(parent) logger.add(self.log) settings = QSettings() self.mainlayout = QVBoxLayout() self.mainlayout.setContentsMargins(5, 5, 5, 5) self.setLayout(self.mainlayout) # summary summarylayout = FlowLayout() summarylayout.setContentsMargins(0, 0, 0, 0) self.summary = QWidget() self.summary.setLayout(summarylayout) self.mainlayout.addWidget(self.summary) self.summary.setVisible( settings.value('showSummary', 'True') == 'True') detailslayout = QHBoxLayout() detailslayout.setContentsMargins(1, 0, 0, 0) detailslayout.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) detailslayout.setSpacing(15) details = QWidget() details.setLayout(detailslayout) summarylayout.addWidget(details) self.modstotal = QLabel() detailslayout.addWidget(self.modstotal) self.modsenabled = QLabel() detailslayout.addWidget(self.modsenabled) self.overridden = QLabel() detailslayout.addWidget(self.overridden) self.conflicts = QLabel() detailslayout.addWidget(self.conflicts) buttonslayout = QHBoxLayout() buttonslayout.setContentsMargins(0, 0, 0, 0) buttonslayout.setAlignment(Qt.AlignRight | Qt.AlignVCenter) buttons = QWidget() buttons.setLayout(buttonslayout) summarylayout.addWidget(buttons) self.startscriptmerger = QPushButton('Start Script Merger') self.startscriptmerger.setContentsMargins(0, 0, 0, 0) self.startscriptmerger.setMinimumWidth(140) self.startscriptmerger.setIcon( QIcon(str(getRuntimePath('resources/icons/script.ico')))) self.startscriptmerger.clicked.connect(lambda: [ openExecutable(Path(str(settings.value('scriptMergerPath'))), True) ]) self.startscriptmerger.setEnabled( verifyScriptMergerPath( Path(str(settings.value('scriptMergerPath')))) is not None) buttonslayout.addWidget(self.startscriptmerger) self.startgame = QPushButton('Start Game') self.startgame.setContentsMargins(0, 0, 0, 0) self.startgame.setMinimumWidth(100) self.startgame.setIcon( QIcon(str(getRuntimePath('resources/icons/w3b.ico')))) buttonslayout.addWidget(self.startgame) # splitter self.splitter = QSplitter(Qt.Vertical) self.stack = QStackedWidget() self.splitter.addWidget(self.stack) # mod list widget self.modlistwidget = QWidget() self.modlistlayout = QVBoxLayout() self.modlistlayout.setContentsMargins(0, 0, 0, 0) self.modlistwidget.setLayout(self.modlistlayout) self.stack.addWidget(self.modlistwidget) # search bar self.searchbar = QLineEdit() self.searchbar.setPlaceholderText('Search...') self.modlistlayout.addWidget(self.searchbar) # mod list self.modlist = ModList(self, model) self.modlistlayout.addWidget(self.modlist) self.searchbar.textChanged.connect(lambda e: self.modlist.setFilter(e)) # welcome message welcomelayout = QVBoxLayout() welcomelayout.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) welcomewidget = QWidget() welcomewidget.setLayout(welcomelayout) welcomewidget.dragEnterEvent = self.modlist.dragEnterEvent # type: ignore welcomewidget.dragMoveEvent = self.modlist.dragMoveEvent # type: ignore welcomewidget.dragLeaveEvent = self.modlist.dragLeaveEvent # type: ignore welcomewidget.dropEvent = self.modlist.dropEvent # type: ignore welcomewidget.setAcceptDrops(True) icon = QIcon(str(getRuntimePath('resources/icons/open-folder.ico'))) iconpixmap = icon.pixmap(32, 32) icon = QLabel() icon.setPixmap(iconpixmap) icon.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) icon.setContentsMargins(4, 4, 4, 4) welcomelayout.addWidget(icon) welcome = QLabel('''<p><font> No mod installed yet. Drag a mod into this area to get started! </font></p>''') welcome.setAttribute(Qt.WA_TransparentForMouseEvents) welcome.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) welcomelayout.addWidget(welcome) self.stack.addWidget(welcomewidget) # output log self.output = QTextEdit(self) self.output.setTextInteractionFlags(Qt.NoTextInteraction) self.output.setReadOnly(True) self.output.setContextMenuPolicy(Qt.NoContextMenu) self.output.setPlaceholderText('Program output...') self.splitter.addWidget(self.output) # TODO: enhancement: show indicator if scripts have to be merged self.splitter.setStretchFactor(0, 1) self.splitter.setStretchFactor(1, 0) self.mainlayout.addWidget(self.splitter) # TODO: incomplete: make start game button functional if len(model): self.stack.setCurrentIndex(0) self.splitter.setSizes([self.splitter.size().height(), 50]) else: self.stack.setCurrentIndex(1) self.splitter.setSizes([self.splitter.size().height(), 0]) model.updateCallbacks.append(self.modelUpdateEvent) asyncio.create_task(model.loadInstalled()) def keyPressEvent(self, event: QKeyEvent) -> None: if event.key() == Qt.Key_Escape: self.modlist.setFocus() self.searchbar.setText('') elif event.matches(QKeySequence.Find): self.searchbar.setFocus() elif event.matches(QKeySequence.Paste): self.pasteEvent() # TODO: enhancement: add start game / start script merger shortcuts else: super().keyPressEvent(event) def pasteEvent(self) -> None: clipboard = QApplication.clipboard().text().splitlines() if len(clipboard) == 1 and isValidNexusModsUrl(clipboard[0]): self.parentWidget().showDownloadModDialog() else: urls = [ url for url in QApplication.clipboard().text().splitlines() if len(str(url.strip())) ] if all( isValidModDownloadUrl(url) or isValidFileUrl(url) for url in urls): asyncio.create_task(self.modlist.checkInstallFromURLs(urls)) def modelUpdateEvent(self, model: Model) -> None: total = len(model) enabled = len([mod for mod in model if model[mod].enabled]) overridden = sum( len(file) for file in model.conflicts.bundled.values()) conflicts = sum(len(file) for file in model.conflicts.scripts.values()) self.modstotal.setText( f'<font color="#73b500" size="4">{total}</font> \ <font color="#888" text-align="center">Installed Mod{"" if total == 1 else "s"}</font>' ) self.modsenabled.setText( f'<font color="#73b500" size="4">{enabled}</font> \ <font color="#888">Enabled Mod{"" if enabled == 1 else "s"}</font>' ) self.overridden.setText( f'<font color="{"#b08968" if overridden > 0 else "#84C318"}" size="4">{overridden}</font> \ <font color="#888">Overridden File{"" if overridden == 1 else "s"}</font> ' ) self.conflicts.setText( f'<font color="{"#E55934" if conflicts > 0 else "#aad576"}" size="4">{conflicts}</font> \ <font color="#888">Unresolved Conflict{"" if conflicts == 1 else "s"}</font> ' ) if len(model) > 0: if self.stack.currentIndex() != 0: self.stack.setCurrentIndex(0) self.repaint() else: if self.stack.currentIndex() != 1: self.stack.setCurrentIndex(1) self.repaint() def unhideOutput(self) -> None: if self.splitter.sizes()[1] < 10: self.splitter.setSizes([self.splitter.size().height(), 50]) def unhideModList(self) -> None: if self.splitter.sizes()[0] < 10: self.splitter.setSizes([50, self.splitter.size().height()]) def log(self, message: Any) -> None: # format log messages to user readable output settings = QSettings() record = message.record message = record['message'] extra = record['extra'] level = record['level'].name.lower() name = str(extra['name'] ) if 'name' in extra and extra['name'] is not None else '' path = str(extra['path'] ) if 'path' in extra and extra['path'] is not None else '' dots = bool( extra['dots'] ) if 'dots' in extra and extra['dots'] is not None else False newline = bool( extra['newline'] ) if 'newline' in extra and extra['newline'] is not None else False output = bool( extra['output'] ) if 'output' in extra and extra['output'] is not None else bool( message) modlist = bool( extra['modlist'] ) if 'modlist' in extra and extra['modlist'] is not None else False if level in ['debug' ] and settings.value('debugOutput', 'False') != 'True': if newline: self.output.append(f'') return n = '<br>' if newline else '' d = '...' if dots else '' if len(name) and len(path): path = f' ({path})' if output: message = html.escape(message, quote=True) if level in ['success', 'error', 'warning']: message = f'<strong>{message}</strong>' if level in ['success']: message = f'<font color="#04c45e">{message}</font>' if level in ['error', 'critical']: message = f'<font color="#ee3b3b">{message}</font>' if level in ['warning']: message = f'<font color="#ff6500">{message}</font>' if level in ['debug', 'trace']: message = f'<font color="#aaa">{message}</font>' path = f'<font color="#aaa">{path}</font>' if path else '' d = f'<font color="#aaa">{d}</font>' if d else '' time = record['time'].astimezone( tz=None).strftime('%Y-%m-%d %H:%M:%S') message = f'<font color="#aaa">{time}</font> {message}' self.output.append( f'{n}{message.strip()}{" " if name or path else ""}{name}{path}{d}' ) else: self.output.append(f'') self.output.verticalScrollBar().setValue( self.output.verticalScrollBar().maximum()) self.output.repaint() if modlist: self.unhideModList() if settings.value('unhideOutput', 'True') == 'True' and output: self.unhideOutput()
class BuilderWidget(QWidget): def __init__(self, parent=None) -> None: super().__init__(parent) self.verticalLayout = QVBoxLayout(self) self.compileButton = QPushButton('Compile', self) self.compileButton.setMinimumSize(QSize(200, 50)) self.verticalLayout.addWidget(self.compileButton, 0, Qt.AlignHCenter) self.tidyButton = QPushButton('Tidy', self) self.verticalLayout.addWidget(self.tidyButton, 0, Qt.AlignHCenter) self.splitter = QSplitter(self) self.splitter.setOrientation(Qt.Vertical) self.widget = QWidget(self.splitter) self.widget.setObjectName(u"widget") self.verticalLayout2 = QVBoxLayout(self.widget) self.stdoutLabel = QLabel(self.widget) self.stdoutLabel.setText('stdout:') self.verticalLayout2.addWidget(self.stdoutLabel) self.stdoutText = QTextEdit(self.widget) self.stdoutText.setEnabled(False) self.stdoutText.setReadOnly(True) self.stdoutText.setPlainText("Output will appear here") self.verticalLayout2.addWidget(self.stdoutText) self.splitter.addWidget(self.widget) self.widget1 = QWidget(self.splitter) self.verticalLayout3 = QVBoxLayout(self.widget1) self.stderrLabel = QLabel(self.widget1) self.stderrLabel.setText('stderr:') self.verticalLayout3.addWidget(self.stderrLabel) self.stderrText = QTextEdit(self.widget1) self.stderrText.setEnabled(False) self.stderrText.setReadOnly(True) self.stderrText.setPlainText('Errors will appear here') self.verticalLayout3.addWidget(self.stderrText) self.splitter.addWidget(self.widget1) self.verticalLayout.addWidget(self.splitter) # Logic # Use QProcess to start a program and get its outputs https://stackoverflow.com/a/22110924 self.process = QProcess(self) self.process.readyReadStandardOutput.connect(self.readStdout) self.process.readyReadStandardError.connect(self.readStderr) self.process.started.connect(self.processStarted) self.process.finished.connect(self.processFinished) self.process.errorOccurred.connect(self.errorOccurred) self.compileButton.clicked.connect(self.doCompile) self.tidyButton.clicked.connect(self.doTidy) def doCompile(self): self.cleanupUI() self.process.setWorkingDirectory(settings.get_repo_location()) self.process.startCommand(settings.get_build_command()) def doTidy(self): self.cleanupUI() self.process.setWorkingDirectory(settings.get_repo_location()) self.process.startCommand(settings.get_tidy_command()) def cleanupUI(self): self.stdoutText.setEnabled(True) self.stderrText.setEnabled(True) self.stdoutText.setPlainText('') self.stderrText.setPlainText('') def readStdout(self): lines = self.process.readAllStandardOutput().data().decode( )[:-1].split('\n') for line in lines: if line == 'tmc.gba: FAILED': line = 'tmc.gba: <b style="color:red">FAILED</b>' elif line == 'tmc.gba: OK': line = 'tmc.gba: <b style="color:lime">OK</b>' self.stdoutText.append(line) def readStderr(self): lines = self.process.readAllStandardError().data().decode()[:-1].split( '\n') for line in lines: if 'error' in line.lower(): line = f'<span style="color:red">{line}</span>' elif 'warning' in line.lower(): line = f'<span style="color:orange">{line}</span>' self.stderrText.append(line) def processStarted(self): self.compileButton.setEnabled(False) self.tidyButton.setEnabled(False) def processFinished(self): self.compileButton.setEnabled(True) self.tidyButton.setEnabled(True) def errorOccurred(self): self.stderrText.insertPlainText(self.process.errorString())
class MainWindow(QMainWindow): def __init__(self, window_width=None, window_height=None): super(MainWindow, self).__init__() if window_width != None and window_height != None: self.window_width = window_width self.window_height = window_height self.resize(window_width, window_height) self.stack_of_widget = QStackedWidget() self.central_widget = QWidget() self.main_layout = QVBoxLayout() self.username = "" # ui widgets self.message_visable_field = QTextEdit() self.message_input_field = QTextEdit() self.send_message = QPushButton("Send my message!") self.login_screen = LoginScreen() self.login_screen.auth_button.clicked.connect(self.login_button_click) # show all widgets self.build_ui() def build_ui(self): self.message_visable_field.setFont(QFont("Roboto", 12)) self.message_visable_field.setReadOnly(True) self.message_input_field.setFont(QFont("Roboto", 11)) self.message_input_field.setPlaceholderText( "Input your message here...") self.message_input_field.setMinimumHeight(50) self.message_input_field.setMaximumHeight(50) self.send_message.setFont(QFont("Roboto", 11)) self.send_message.clicked.connect(self.send_message_button_clicked) self.main_layout.addWidget(self.message_visable_field) self.main_layout.addWidget(self.message_input_field) self.main_layout.addWidget(self.send_message) self.central_widget.setLayout(self.main_layout) self.stack_of_widget.addWidget(self.login_screen) self.setMaximumSize(self.minimumSize()) self.stack_of_widget.addWidget(self.central_widget) self.setCentralWidget(self.stack_of_widget) def send_message_button_clicked(self): """ send a client message to the server, and append to the list message. """ message = self.message_input_field.toPlainText() if message != '': # send message to the server r_type = RequestInfo(type_request="MessageRequest", request_ts=f"{datetime.now()}") r_data = MessageRequest(from_=self.username, to="all", message=message) request = Request(request=[r_type], data=[r_data]) client_worker.send(request.json()) self.message_visable_field.append('You: ' + message) self.message_input_field.clear() def login_button_click(self): self.username = self.login_screen.login_field.text() r_type = RequestInfo(type_request="AuthRequest", request_ts=f"{datetime.now()}") r_data = AuthRequest( login=f"{self.username}", password=f"{self.login_screen.password_field.text()}") request = Request(request=[r_type], data=[r_data]) client_worker.send(request.json()) @Slot(str) def recieved_message_handler(self, message): msg = json.loads(message) print(msg) response = Request(**msg) if response.request[0].type_request == "AuthResponse": data = AuthResponse(**response.data[0]) if data.access is True: self.show_main_screen() elif response.request[0].type_request == "MessageRequest": data = MessageRequest(**response.data[0]) if data.to == "all": self.message_visable_field.append( f"[{data.from_}]: {data.message}") def show_main_screen(self): self.stack_of_widget.setCurrentWidget(self.central_widget) self.setMinimumSize(self.window_width, self.window_height) self.resize(self.minimumSize()) def closeEvent(self, event): client_worker.close() network_thread.exit() network_thread.wait(500) if network_thread.isRunning() is True: network_thread.terminate()
class RenameFilesGUI(QWidget): def __init__(self): super().__init__() self.init_ui() def init_ui(self): self.setMinimumSize(600, 250) self.setWindowTitle("Change File Names GUI") self.directory = "" self.cb_value = "" self.setupWidgets() self.show() def setupWidgets(self): """ Set up the widgets and layouts for interface. """ dir_label = QLabel("Choose Directory:") self.dir_line_edit = QLineEdit() dir_button = QPushButton('...') dir_button.setToolTip("Select file directory.") dir_button.clicked.connect(self.setDirectory) self.change_name_edit = QLineEdit() self.change_name_edit.setToolTip( "Files will be appended with numerical values.For example: filename <b> 01 </b >.jpg") self.change_name_edit.setPlaceholderText("Change file names to...") rename_button = QPushButton("Rename Files") rename_button.setToolTip("Begin renaming files in directory.") rename_button.clicked.connect(self.renameFiles) file_exts = [".jpg", ".jpeg", ".png", ".gif", ".txt"] # Create combo box for selecting file extensions. ext_cb = QComboBox() self.cb_value = file_exts[0] ext_cb.setToolTip("Only files with this extension will be changed.") ext_cb.addItems(file_exts) ext_cb.currentTextChanged.connect(self.updateCbValue) # Text edit is for displaying the file names as they are updated. self.display_files_edit = QTextEdit() self.display_files_edit.setReadOnly(True) self.progress_bar = QProgressBar() self.progress_bar.setValue(0) # Set layout and widgets. grid = QGridLayout() grid.addWidget(dir_label, 0, 0) grid.addWidget(self.dir_line_edit, 1, 0, 1, 2) grid.addWidget(dir_button, 1, 2) grid.addWidget(self.change_name_edit, 2, 0) grid.addWidget(ext_cb, 2, 1) grid.addWidget(rename_button, 2, 2) grid.addWidget(self.display_files_edit, 3, 0, 1, 3) grid.addWidget(self.progress_bar, 4, 0, 1, 3) self.setLayout(grid) def setDirectory(self): """ Choose the directory. """ file_dialog = QFileDialog(self) file_dialog.setFileMode(QFileDialog.Directory) self.directory = file_dialog.getExistingDirectory(self, "Open Directory", "", QFileDialog.ShowDirsOnly) if self.directory: self.dir_line_edit.setText(self.directory) # Set the max value of progress bar equal to max number of files in the directory. num_of_files = len([name for name in os.listdir(self.directory)]) self.progress_bar.setRange(0, num_of_files) def updateCbValue(self, text): """ Change the combo box value. Values represent the different file extensions. """ self.cb_value = text def renameFiles(self): """ Create instance of worker thread to handle the file renaming process. """ prefix_text = self.change_name_edit.text() if self.directory != "" and prefix_text != "": self.worker = Worker(self.directory, self.cb_value, prefix_text) self.worker.updateValueSignal.connect(self.updateProgressBar) self.worker.updateTextEditSignal.connect(self.updateTextEdit) self.worker.start() else: pass def updateProgressBar(self, value): self.progress_bar.setValue(value) def updateTextEdit(self, old_text, new_text): self.display_files_edit.append("[INFO] {} changed to{}.".format(old_text, new_text))