class TextLogElement(object): def __init__(self, maximum_block_count: int = 1000, font_size_pt: int = 10, font_family: str = "Courier", title: str = "Log") -> None: # For nested layouts: (1) create everything, (2) lay out self.log_group = StyledQGroupBox(title) log_layout_1 = QVBoxLayout() log_layout_2 = QHBoxLayout() self.log = QPlainTextEdit() self.log.setReadOnly(True) self.log.setLineWrapMode(QPlainTextEdit.NoWrap) self.log.setMaximumBlockCount(maximum_block_count) font = self.log.font() font.setFamily(font_family) font.setPointSize(font_size_pt) log_clear_button = QPushButton('Clear log') log_clear_button.clicked.connect(self.log.clear) log_copy_button = QPushButton('Copy to clipboard') log_copy_button.clicked.connect(self.copy_whole_log) log_layout_2.addWidget(log_clear_button) log_layout_2.addWidget(log_copy_button) log_layout_2.addStretch(1) log_layout_1.addWidget(self.log) log_layout_1.addLayout(log_layout_2) self.log_group.setLayout(log_layout_1) def get_widget(self) -> QWidget: return self.log_group def add(self, msg: str) -> None: # http://stackoverflow.com/questions/16568451 # self.log.moveCursor(QTextCursor.End) self.log.appendPlainText(msg) # ... will append it as a *paragraph*, i.e. no need to add a newline # self.scroll_to_end_of_log() def copy_whole_log(self) -> None: # Ctrl-C will copy the selected parts. # log.copy() will copy the selected parts. self.log.selectAll() self.log.copy() self.log.moveCursor(QTextCursor.End) self.scroll_to_end_of_log() def scroll_to_end_of_log(self) -> None: vsb = self.log.verticalScrollBar() vsb.setValue(vsb.maximum()) hsb = self.log.horizontalScrollBar() hsb.setValue(0)
class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.setGeometry(0, 0, 700, 400) self.setContentsMargins(6, 6, 6, 6) self.setStyleSheet(myStyleSheet(self)) self.setWindowTitle("Radio Stations - searching with pyradios") self.genreList = genres.splitlines() self.findfield = QLineEdit() self.findfield.setFixedWidth(250) self.findfield.addAction(QIcon.fromTheme("edit-find"), 0) self.findfield.setPlaceholderText("type search term and press RETURN ") self.findfield.returnPressed.connect(self.findStations) self.findfield.setClearButtonEnabled(True) self.field = QPlainTextEdit() self.field.setContextMenuPolicy(Qt.CustomContextMenu) self.field.customContextMenuRequested.connect( self.contextMenuRequested) self.field.cursorPositionChanged.connect(self.selectLine) self.field.setWordWrapMode(QTextOption.NoWrap) ### volume slider self.volSlider = QSlider() self.volSlider.setFixedWidth(100) self.volSlider.setOrientation(Qt.Horizontal) self.volSlider.valueChanged.connect(self.setVolume) self.volSlider.setMinimum(0) self.volSlider.setMaximum(100) ### genre box self.combo = QComboBox() self.combo.currentIndexChanged.connect(self.comboSearch) self.combo.addItem("choose Genre") for m in self.genreList: self.combo.addItem(m) self.combo.addItem("Country") self.combo.setFixedWidth(150) ### toolbar ### self.tb = self.addToolBar("tools") self.tb.setContextMenuPolicy(Qt.PreventContextMenu) self.tb.setMovable(False) self.saveButton = QPushButton("Save as txt") self.saveButton.setIcon(QIcon.fromTheme("document-save")) self.saveButton.clicked.connect(self.saveStations) self.savePlaylistButton = QPushButton("Save as m3u") self.savePlaylistButton.setIcon(QIcon.fromTheme("document-save")) self.savePlaylistButton.clicked.connect(self.savePlaylist) self.setCentralWidget(self.field) self.tb.addWidget(self.findfield) self.tb.addWidget(self.saveButton) self.tb.addWidget(self.savePlaylistButton) self.tb.addSeparator() self.tb.addWidget(self.combo) ### player ### self.player = QMediaPlayer() self.player.metaDataChanged.connect(self.metaDataChanged) self.startButton = QPushButton("Play") self.startButton.setIcon(QIcon.fromTheme("media-playback-start")) self.startButton.clicked.connect(self.getURLtoPlay) self.stopButton = QPushButton("Stop") self.stopButton.setIcon(QIcon.fromTheme("media-playback-stop")) self.stopButton.clicked.connect(self.stopPlayer) self.statusBar().addPermanentWidget(self.volSlider) self.statusBar().addPermanentWidget(self.startButton) self.statusBar().addPermanentWidget(self.stopButton) ## actions self.getNameAction = QAction(QIcon.fromTheme("edit-copy"), "copy Station Name", self, triggered=self.getName) self.getUrlAction = QAction(QIcon.fromTheme("edit-copy"), "copy Station URL", self, triggered=self.getURL) self.getNameAndUrlAction = QAction(QIcon.fromTheme("edit-copy"), "copy Station Name,URL", self, triggered=self.getNameAndUrl) self.getURLtoPlayAction = QAction( QIcon.fromTheme("media-playback-start"), "play Station", self, shortcut="F6", triggered=self.getURLtoPlay) self.addAction(self.getURLtoPlayAction) self.stopPlayerAction = QAction(QIcon.fromTheme("media-playback-stop"), "stop playing", self, shortcut="F7", triggered=self.stopPlayer) self.addAction(self.stopPlayerAction) self.helpAction = QAction(QIcon.fromTheme("help-info"), "Help", self, shortcut="F1", triggered=self.showHelp) self.addAction(self.helpAction) self.getForWebAction = QAction(QIcon.fromTheme("browser"), "copy for WebPlayer", self, triggered=self.getForWeb) self.volSlider.setValue(60) self.statusBar().showMessage("Welcome", 0) def setVolume(self): self.player.setVolume(self.volSlider.value()) def comboSearch(self): if self.combo.currentIndex() > 0: self.findfield.setText(self.combo.currentText()) self.findStations() def getName(self): t = self.field.textCursor().selectedText().partition(",")[0] clip = QApplication.clipboard() clip.setText(t) def getURL(self): t = self.field.textCursor().selectedText().partition(",")[2] clip = QApplication.clipboard() clip.setText(t) def getNameAndUrl(self): t = self.field.textCursor().selectedText() clip = QApplication.clipboard() clip.setText(t) def getForWeb(self): t = self.field.textCursor().selectedText() name = t.partition(",")[0] url = t.partition(",")[2] result = f"<li><a class='chlist' href='{url}'>{name}</a></li>" clip = QApplication.clipboard() clip.setText(result) def selectLine(self): tc = self.field.textCursor() tc.select(QTextCursor.LineUnderCursor) tc.movePosition(QTextCursor.StartOfLine, QTextCursor.MoveAnchor) ##, tc.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) self.field.setTextCursor(tc) def showHelp(self): QMessageBox.information( self, "Information", "F6 -> play Station (from line where cursor is)\nF7 -> stop playing" ) def stopPlayer(self): self.player.stop() self.statusBar().showMessage("Player stopped", 0) ### QPlainTextEdit contextMenu def contextMenuRequested(self, point): cmenu = QMenu() if not self.field.toPlainText() == "": cmenu.addAction(self.getNameAction) cmenu.addAction(self.getUrlAction) cmenu.addAction(self.getNameAndUrlAction) cmenu.addSeparator() cmenu.addAction(self.getURLtoPlayAction) cmenu.addAction(self.stopPlayerAction) cmenu.addSeparator() cmenu.addAction(self.helpAction) cmenu.addAction(self.getForWebAction) cmenu.exec_(self.field.mapToGlobal(point)) def getURLtoPlay(self): url = "" tc = self.field.textCursor() rtext = tc.selectedText().partition(",")[2] stext = tc.selectedText().partition(",")[0] url = rtext print("stream url=", url) self.player.setMedia(QMediaContent(QUrl(url))) self.player.play() self.statusBar().showMessage("%s %s" % ("playing", stext), 0) def metaDataChanged(self): if self.player.isMetaDataAvailable(): trackInfo = (self.player.metaData("Title")) trackInfo2 = (self.player.metaData("Comment")) if not trackInfo == None: self.statusBar().showMessage(trackInfo, 0) if not trackInfo2 == None: self.statusBar().showMessage("%s %s" % (trackInfo, trackInfo2)) def findStations(self): self.field.setPlainText("") my_value = self.findfield.text() self.statusBar().showMessage("searching ...") base_url = "https://de1.api.radio-browser.info/xml/stations/byname/" url = f"{base_url}{my_value}" xml = requests.get(url).content.decode() if xml: root = ET.fromstring(xml) for child in root: ch_name = child.attrib["name"] ch_url = child.attrib["url"] self.field.appendPlainText(f"{ch_name},{ch_url}") self.copyToClipboard() tc = self.field.textCursor() tc.movePosition(QTextCursor.Start, QTextCursor.MoveAnchor) tc.select(QTextCursor.LineUnderCursor) tc.movePosition(QTextCursor.EndOfBlock, QTextCursor.KeepAnchor) self.field.setTextCursor(tc) else: self.statusBar().showMessage("nothing found", 0) self.field.verticalScrollBar().triggerAction( QScrollBar.SliderToMinimum) self.field.horizontalScrollBar().triggerAction( QScrollBar.SliderToMinimum) def saveStations(self): if not self.field.toPlainText() == "": path, _ = QFileDialog.getSaveFileName( None, "RadioStations", self.findfield.text() + ".txt", "Text Files (*.txt)") if path: s = self.field.toPlainText() with open(path, 'w') as f: f.write(s) f.close() self.statusBar().showMessage("saved!", 0) def savePlaylist(self): if not self.field.toPlainText() == "": path, _ = QFileDialog.getSaveFileName( None, "RadioStations", self.findfield.text() + ".m3u", "Playlist Files (*.m3u)") if path: result = "" s = self.field.toPlainText() st = [] for line in s.splitlines(): st.append(line) result += "#EXTM3U" result += '\n' for x in range(len(st)): result += "#EXTINF:" + str(x) + "," + st[x].partition( ",")[0] result += '\n' result += st[x].partition(",")[2] result += '\n' with open(path, 'w') as f: f.write(result) f.close() self.statusBar().showMessage("saved!", 0) def copyToClipboard(self): clip = QApplication.clipboard() if not self.field.toPlainText() == "": clip.setText(self.field.toPlainText())
class LogWindow(QMainWindow): emit_msg = pyqtSignal(str) def __init__(self, level: int = logging.INFO, window_title: str = "Python log", logger: logging.Logger = None, min_width: int = 800, min_height: int = 400, maximum_block_count: int = 1000) -> None: super().__init__() self.setStyleSheet(LOGEDIT_STYLESHEET) self.handler = HtmlColorHandler(self.log_message, level) self.may_close = False self.set_may_close(self.may_close) self.setWindowTitle(window_title) if min_width: self.setMinimumWidth(min_width) if min_height: self.setMinimumHeight(min_height) log_group = StyledQGroupBox("Log") log_layout_1 = QVBoxLayout() log_layout_2 = QHBoxLayout() self.log = QPlainTextEdit() # QPlainTextEdit better than QTextEdit because it supports # maximumBlockCount while still allowing HTML (via appendHtml, # not insertHtml). self.log.setReadOnly(True) self.log.setLineWrapMode(QPlainTextEdit.NoWrap) self.log.setMaximumBlockCount(maximum_block_count) log_clear_button = QPushButton('Clear log') log_clear_button.clicked.connect(self.log.clear) log_copy_button = QPushButton('Copy to clipboard') log_copy_button.clicked.connect(self.copy_whole_log) log_layout_2.addWidget(log_clear_button) log_layout_2.addWidget(log_copy_button) log_layout_2.addStretch() log_layout_1.addWidget(self.log) log_layout_1.addLayout(log_layout_2) log_group.setLayout(log_layout_1) main_widget = QWidget(self) self.setCentralWidget(main_widget) main_layout = QVBoxLayout(main_widget) main_layout.addWidget(log_group) self.emit_msg.connect(self.log_internal) if logger: logger.addHandler(self.get_handler()) def get_handler(self) -> logging.Handler: return self.handler def set_may_close(self, may_close: bool) -> None: # log.debug("LogWindow: may_close({})".format(may_close)) self.may_close = may_close # return if may_close: self.setWindowFlags(self.windowFlags() | Qt.WindowCloseButtonHint) else: self.setWindowFlags(self.windowFlags() & ~Qt.WindowCloseButtonHint) self.show() # ... or it will be hidden (in a logical not a real way!) by # setWindowFlags(), and thus mess up the logic for the whole Qt app # exiting (since qt_app.exec_() runs until there are no more windows # being shown). def copy_whole_log(self) -> None: # Ctrl-C will copy the selected parts. # log.copy() will copy the selected parts. self.log.selectAll() self.log.copy() self.log.moveCursor(QTextCursor.End) self.scroll_to_end_of_log() def scroll_to_end_of_log(self) -> None: vsb = self.log.verticalScrollBar() vsb.setValue(vsb.maximum()) hsb = self.log.horizontalScrollBar() hsb.setValue(0) # noinspection PyPep8Naming def closeEvent(self, event: QCloseEvent) -> None: """Trap exit.""" if not self.may_close: # log.debug("LogWindow: ignore closeEvent") event.ignore() else: # log.debug("LogWindow: accept closeEvent") event.accept() def log_message(self, html: str) -> None: # Jump threads via a signal self.emit_msg.emit(html) @pyqtSlot(str) def log_internal(self, html: str) -> None: # self.log.moveCursor(QTextCursor.End) # self.log.insertHtml(html) self.log.appendHtml(html) # self.scroll_to_end_of_log() # ... unnecessary; if you're at the end, it scrolls, and if you're at # the top, it doesn't bug you. @pyqtSlot() def exit(self) -> None: # log.debug("LogWindow: exit") self.may_close = True # closed = QMainWindow.close(self) # log.debug("closed: {}".format(closed)) QMainWindow.close(self) @pyqtSlot() def may_exit(self) -> None: # log.debug("LogWindow: may_exit") self.set_may_close(True)