def help(self): """ Open help page from https://pyfda.rtfd.org in browser """ url = QtCore.QUrl('https://pyfda.readthedocs.io/en/latest/' + self.a_he.info) if not url.isValid(): logger.warning("Invalid URL\n\t{0}\n\tOpening " "'https://pyfda.readthedocs.io/en/latest/' instead".format(url.toString())) url = QtCore.QUrl('https://pyfda.readthedocs.io/en/latest/') #if url.isLocalFile() QtGui.QDesktopServices.openUrl(url)
def mpl2Clip(self): """ Copy current figure to the clipboard, either directly as PNG file or as base64 encoded PNG file. """ try: img = QImage(self.canvas.grab()) modifiers = QtWidgets.QApplication.keyboardModifiers() if modifiers in { QtCore.Qt.ShiftModifier, QtCore.Qt.ControlModifier }: ba = QtCore.QByteArray() buffer = QtCore.QBuffer(ba) # create buffer of ByteArray buffer.open(QtCore.QIODevice.WriteOnly) # ... open it img.save(buffer, 'PNG') # ... save image data to buffer # ... convert to base64 bytes -> str -> strip b' ... ' base64_str = str(ba.toBase64().data()).lstrip("b'").rstrip("'") # ... and copy as string to clipboard after removing b' ... ' if modifiers == QtCore.Qt.ShiftModifier: self.cb.setText(base64_str) logger.info( 'Copied plot as base64 encoded PNG image to Clipboard.' ) elif modifiers == QtCore.Qt.ControlModifier: self.cb.setText('<img src="data:image/png;base64,' + base64_str + '"/>') logger.info( 'Copied plot as base64 encoded PNG image with <img> tag ' 'to Clipboard.') # elif modifiers == (QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier): # logger.warning('Control+Shift+Click') else: self.cb.setImage(img) logger.info('Copied plot as PNG image to Clipboard.') except: logger.error('Error copying figure to clipboard:\n{0}'.format( sys.exc_info()))
def text(self, item): """ Return item text as string transformed by self.displayText() """ # return qstr(item.text()) # convert to "normal" string return qstr(self.displayText(item.text(), QtCore.QLocale()))
def _construct_UI(self): """ Initialize UI with tabbed subwidgets: Instantiate dynamically each widget from the dict `fb.plot_classes` and try to - set the TabToolTip from the instance attribute `tool_tip` - set the tab label from the instance attribute `tab_label` for each widget. - connect the available signals of all subwidgets (not all widgets have both `sig_rx` and `sig_tx` signals). - `self.sig_rx` is distributed to all `inst.sig_rx` signals - all `inst.sig_tx` signals are collected in `self.sig_tx` - `self.sig_tx.connect(self.sig_rx)` distributes incoming signals (via pyfdax or coming from the input widgets) among all input widgets. In order to prevent infinite loops, every widget needs to block in- coming signals with its own name! """ tabWidget = QTabWidget(self) tabWidget.setObjectName("plot_tabs") n_wdg = 0 # number and ... inst_wdg_str = "" # ... full names of successfully instantiated plot widgets # for plot_class in fb.plot_classes: try: mod_fq_name = fb.plot_classes[plot_class][ 'mod'] # fully qualified module name mod = importlib.import_module( mod_fq_name) # import plot widget module wdg_class = getattr(mod, plot_class) # get plot widget class ... # and instantiate it inst = wdg_class(self) except ImportError as e: logger.warning('Class "{0}" could not be imported from {1}:\n{2}.'\ .format(plot_class, mod_fq_name, e)) continue # unsuccessful, try next widget if hasattr(inst, 'tab_label'): tabWidget.addTab(inst, inst.tab_label) else: tabWidget.addTab(inst, "not set") if hasattr(inst, 'tool_tip'): tabWidget.setTabToolTip(n_wdg, inst.tool_tip) if hasattr(inst, 'sig_tx'): inst.sig_tx.connect(self.sig_tx) if hasattr(inst, 'sig_rx'): self.sig_rx.connect(inst.sig_rx) n_wdg += 1 # successfully instantiated one more widget inst_wdg_str += '\t' + mod_fq_name + "." + plot_class + '\n' if len(inst_wdg_str) == 0: logger.warning("No plotting widgets found!") else: logger.debug("Imported {0:d} plotting classes:\n{1}".format( n_wdg, inst_wdg_str)) #---------------------------------------------------------------------- layVMain = QVBoxLayout() layVMain.addWidget(tabWidget) layVMain.setContentsMargins( *params['wdg_margins']) #(left, top, right, bottom) self.setLayout(layVMain) #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- # self.sig_rx.connect(inst.sig_rx) # this happens in _construct_UI() #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.timer_id = QtCore.QTimer() self.timer_id.setSingleShot(True) # redraw current widget at timeout (timer was triggered by resize event): self.timer_id.timeout.connect(self.current_tab_redraw) self.sig_tx.connect(self.sig_rx) # loop back to local inputs # self.sig_rx.connect(self.log_rx) # enable for debugging # When user has selected a different tab, trigger a redraw of current tab tabWidget.currentChanged.connect(self.current_tab_changed) # The following does not work: maybe current scope must be left? # tabWidget.currentChanged.connect(tabWidget.currentWidget().redraw) tabWidget.installEventFilter(self) """
class Input_Files(QWidget): """ Create the widget for saving / loading data """ sig_tx = QtCore.pyqtSignal( object) # sent when loading filter ('data_changed') def __init__(self, parent): super(Input_Files, self).__init__(parent) self.tab_label = 'Files' self.tool_tip = "Load and save filter designs." self._construct_UI() def _construct_UI(self): """ Intitialize the user interface - """ # widget / subwindow for parameter selection self.butSave = QPushButton("Save Filter", self) self.butLoad = QPushButton("Load Filter", self) self.butAbout = QPushButton("About", self) # ============== UI Layout ===================================== bfont = QFont() bfont.setBold(True) bifont = QFont() bifont.setBold(True) bifont.setItalic(True) ifont = QFont() ifont.setItalic(True) layVIO = QVBoxLayout() layVIO.addWidget(self.butSave) # save filter dict -> various formats layVIO.addWidget(self.butLoad) # load filter dict -> various formats layVIO.addWidget(self.butAbout) # pop-up "About" window # This is the top level widget, encompassing the other widgets frmMain = QFrame(self) frmMain.setLayout(layVIO) layVMain = QVBoxLayout() layVMain.setAlignment(Qt.AlignTop) # layVMain.addLayout(layVIO) layVMain.addWidget(frmMain) layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(layVMain) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.butSave.clicked.connect(self.save_filter) self.butLoad.clicked.connect(self.load_filter) self.butAbout.clicked.connect(self.about_window) #------------------------------------------------------------------------------ def load_filter(self): """ Load filter from zipped binary numpy array or (c)pickled object to filter dictionary and update input and plot widgets """ file_filters = ("Zipped Binary Numpy Array (*.npz);;Pickled (*.pkl)") dlg = QFD(self) file_name, file_type = dlg.getOpenFileName_(caption="Load filter ", directory=dirs.save_dir, filter=file_filters) file_name = str(file_name) # QString -> str for t in extract_file_ext( file_filters): # get a list of file extensions if t in str(file_type): file_type = t if file_name != "": # cancelled file operation returns empty string if os.stat(file_name).st_size == 0: dirs.save_dir = os.path.dirname(file_name) logger.error( '"{0}" has size zero, aborting.'.format(file_name)) return # strip extension from returned file name (if any) + append file type: file_name = os.path.splitext(file_name)[0] + file_type file_type_err = False fb.fil[1] = fb.fil[0].copy() # backup filter dict try: with io.open(file_name, 'rb') as f: if file_type == '.npz': # http://stackoverflow.com/questions/22661764/storing-a-dict-with-np-savez-gives-unexpected-result # What encoding to use when reading Py2 strings. Only # needed for loading py2 generated pickled files on py3. # fix_imports will try to map old py2 names to new py3 # names when unpickling. a = np.load(f, fix_imports=True, encoding='bytes', allow_pickle=True ) # array containing dict, dtype 'object' logger.debug("Entries in {0}:\n{1}".format( file_name, a.files)) for key in sorted(a): logger.debug("key: {0}|{1}|{2}|{3}".format( key, type(key).__name__, type(a[key]).__name__, a[key])) if np.ndim(a[key]) == 0: # scalar objects may be extracted with the item() method fb.fil[0][key] = a[key].item() else: # array objects are converted to list first fb.fil[0][key] = a[key].tolist() elif file_type == '.pkl': # this only works for python >= 3.3 fb.fil[0] = pickle.load(f, fix_imports=True, encoding='bytes') else: logger.error( 'Unknown file type "{0}"'.format(file_type)) file_type_err = True if not file_type_err: # sanitize values in filter dictionary, keys are ok by now for k in fb.fil[0]: # Bytes need to be decoded for py3 to be used as keys later on if type(fb.fil[0][k]) == bytes: fb.fil[0][k] = fb.fil[0][k].decode('utf-8') if fb.fil[0][k] is None: logger.warning( "Entry fb.fil[0][{0}] is empty!".format(k)) logger.info( 'Successfully loaded filter\n\t"{0}"'.format( file_name)) # emit signal -> InputTabWidgets.load_all: self.sig_tx.emit({ "sender": __name__, 'data_changed': 'filter_loaded' }) dirs.save_dir = os.path.dirname( file_name) # update working dir except IOError as e: logger.error("Failed loading {0}!\n{1}".format(file_name, e)) except Exception as e: logger.error("Unexpected error:\n{0}".format(e)) fb.fil[0] = fb.fil[1] # restore backup #------------------------------------------------------------------------------ def file_dump(self, fOut): """ Dump file out in custom text format that apply tool can read to know filter coef's """ # Fixed format widths for integers and doubles intw = '10' dblW = 27 frcW = 20 # Fill up character string with filter output filtStr = '# IIR filter\n' # parameters that made filter (choose smallest eps) # Amp is stored in Volts (linear units) # the second amp terms aren't really used (for ellip filters) FA_PB = fb.fil[0]['A_PB'] FA_SB = fb.fil[0]['A_SB'] FAmp = min(FA_PB, FA_SB) # Freq terms in radians so move from -1:1 to -pi:pi f_lim = fb.fil[0]['freqSpecsRange'] f_unit = fb.fil[0]['freq_specs_unit'] F_S = fb.fil[0]['f_S'] if fb.fil[0]['freq_specs_unit'] == 'f_S': F_S = F_S * 2 F_SB = fb.fil[0]['F_SB'] * F_S * np.pi F_SB2 = fb.fil[0]['F_SB2'] * F_S * np.pi F_PB = fb.fil[0]['F_PB'] * F_S * np.pi F_PB2 = fb.fil[0]['F_PB2'] * F_S * np.pi # Determine pass/stop bands depending on filter response type passMin = [] passMax = [] stopMin = [] stopMax = [] if fb.fil[0]['rt'] == 'LP': passMin = [-F_PB, 0, 0] passMax = [F_PB, 0, 0] stopMin = [-np.pi, F_SB, 0] stopMax = [-F_SB, np.pi, 0] f1 = F_PB f2 = F_SB f3 = f4 = 0 Ftype = 1 Fname = 'Low_Pass' if fb.fil[0]['rt'] == 'HP': passMin = [-np.pi, F_PB, 0] passMax = [-F_PB, np.pi, 0] stopMin = [-F_SB, 0, 0] stopMax = [F_SB, 0, 0] f1 = F_SB f2 = F_PB f3 = f4 = 0 Ftype = 2 Fname = 'Hi_Pass' if fb.fil[0]['rt'] == 'BS': passMin = [-np.pi, -F_PB, F_PB2] passMax = [-F_PB2, F_PB, np.pi] stopMin = [-F_SB2, F_SB, 0] stopMax = [-F_SB, F_SB2, 0] f1 = F_PB f2 = F_SB f3 = F_SB2 f4 = F_PB2 Ftype = 4 Fname = 'Band_Stop' if fb.fil[0]['rt'] == 'BP': passMin = [-F_PB2, F_PB, 0] passMax = [-F_PB, F_PB2, 0] stopMin = [-np.pi, -F_SB, F_SB2] stopMax = [-F_SB2, F_SB, np.pi] f1 = F_SB f2 = F_PB f3 = F_PB2 f4 = F_SB2 Ftype = 3 Fname = 'Band_Pass' filtStr = filtStr + '{:{align}{width}}'.format( '10', align='>', width=intw) + ' IIRFILT_4SYM\n' filtStr = filtStr + '{:{align}{width}}'.format( str(Ftype), align='>', width=intw) + ' ' + Fname + '\n' filtStr = filtStr + '{:{d}.{p}f}'.format(FAmp, d=dblW, p=frcW) + '\n' filtStr = filtStr + '{: {d}.{p}f}'.format(passMin[0], d=dblW, p=frcW) filtStr = filtStr + '{: {d}.{p}f}'.format(passMax[0], d=dblW, p=frcW) + '\n' filtStr = filtStr + '{: {d}.{p}f}'.format(passMin[1], d=dblW, p=frcW) filtStr = filtStr + '{: {d}.{p}f}'.format(passMax[1], d=dblW, p=frcW) + '\n' filtStr = filtStr + '{: {d}.{p}f}'.format(passMin[2], d=dblW, p=frcW) filtStr = filtStr + '{: {d}.{p}f}'.format(passMax[2], d=dblW, p=frcW) + '\n' filtStr = filtStr + '{: {d}.{p}f}'.format(stopMin[0], d=dblW, p=frcW) filtStr = filtStr + '{: {d}.{p}f}'.format(stopMax[0], d=dblW, p=frcW) + '\n' filtStr = filtStr + '{: {d}.{p}f}'.format(stopMin[1], d=dblW, p=frcW) filtStr = filtStr + '{: {d}.{p}f}'.format(stopMax[1], d=dblW, p=frcW) + '\n' filtStr = filtStr + '{: {d}.{p}f}'.format(stopMin[2], d=dblW, p=frcW) filtStr = filtStr + '{: {d}.{p}f}'.format(stopMax[2], d=dblW, p=frcW) + '\n' filtStr = filtStr + '{: {d}.{p}f}'.format(f1, d=dblW, p=frcW) filtStr = filtStr + '{: {d}.{p}f}'.format(f2, d=dblW, p=frcW) + '\n' filtStr = filtStr + '{: {d}.{p}f}'.format(f3, d=dblW, p=frcW) filtStr = filtStr + '{: {d}.{p}f}'.format(f4, d=dblW, p=frcW) + '\n' # move pol/res/gain into terms we need Fdc = fb.fil[0]['rpk'][2] rC = fb.fil[0]['rpk'][0] pC = fb.fil[0]['rpk'][1] Fnum = len(pC) # Gain term filtStr = filtStr + '{: {d}.{p}e}'.format(Fdc, d=dblW, p=frcW) + '\n' # Real pole count inside the unit circle (none of these) filtStr = filtStr + '{:{align}{width}}'.format( str(0), align='>', width=intw) + '\n' # Complex pole/res count inside the unit circle filtStr = filtStr + '{:{i}d}'.format(Fnum, i=intw) + '\n' # Now dump poles/residues for j in range(Fnum): filtStr = filtStr + '{:{i}d}'.format(j, i=intw) + ' ' filtStr = filtStr + '{: {d}.{p}e}'.format( rC[j].real, d=dblW, p=frcW) + ' ' filtStr = filtStr + '{: {d}.{p}e}'.format( rC[j].imag, d=dblW, p=frcW) + ' ' filtStr = filtStr + '{: {d}.{p}e}'.format( pC[j].real, d=dblW, p=frcW) + ' ' filtStr = filtStr + '{: {d}.{p}e}'.format( pC[j].imag, d=dblW, p=frcW) + '\n' # Real pole count outside the unit circle (none of these) filtStr = filtStr + '{:{align}{width}}'.format( str(0), align='>', width=intw) + '\n' # Complex pole count outside the unit circle (none of these) filtStr = filtStr + '{:{align}{width}}'.format( str(0), align='>', width=intw) + '\n' # Now write huge text string to file fOut.write(filtStr) #------------------------------------------------------------------------------ def save_filter(self): """ Save filter as zipped binary numpy array or pickle object """ file_filters = ( "Zipped Binary Numpy Array (*.npz);;Pickled (*.pkl);;Text file pole/residue (*.txt_rpk)" ) dlg = QFD(self) # return selected file name (with or without extension) and filter (Linux: full text) file_name, file_type = dlg.getSaveFileName_(caption="Save filter as", directory=dirs.save_dir, filter=file_filters) file_name = str(file_name) # QString -> str() needed for Python 2.x # Qt5 has QFileDialog.mimeTypeFilters(), but under Qt4 the mime type cannot # be extracted reproducibly across file systems, so it is done manually: for t in extract_file_ext( file_filters): # get a list of file extensions if t in str(file_type): file_type = t # return the last matching extension if file_name != "": # cancelled file operation returns empty string # strip extension from returned file name (if any) + append file type: file_name = os.path.splitext(file_name)[0] + file_type file_type_err = False try: if file_type == '.txt_rpk': # save as a custom residue/pole text output for apply with custom tool # make sure we have the residues if 'rpk' in fb.fil[0]: with io.open(file_name, 'w', encoding="utf8") as f: self.file_dump(f) else: file_type_err = True logger.error( 'Filter has no residues/poles, cannot save as *.txt_rpk file' ) else: with io.open(file_name, 'wb') as f: if file_type == '.npz': np.savez(f, **fb.fil[0]) elif file_type == '.pkl': # save in default pickle version, only compatible with Python 3.x pickle.dump(fb.fil[0], f, protocol=3) else: file_type_err = True logger.error( 'Unknown file type "{0}"'.format(file_type)) if not file_type_err: logger.info('Successfully saved filter as\n\t"{0}"'.format( file_name)) dirs.save_dir = os.path.dirname(file_name) # save new dir except IOError as e: logger.error('Failed saving "{0}"!\n{1}'.format(file_name, e)) #------------------------------------------------------------------------------ def about_window(self): """ Display an "About" window with copyright and version infos """ def to_clipboard(my_string): """ Copy version info to clipboard """ mapping = [ ('<br>', '\n'), ('<br />', '\n'), ('</tr>', '\n'), ('</th>', '\n==============\n'), ('</table>', '\n'), ('<hr>', '\n---------\n'), ('<b>', ''), ('</b>', ''), ('<tr>', ''), ('<td>', ''), ('</td>', '\t'), ('<th>', ''), (' ', ' '), ('<table>', ''), # ('</a>',''), ("<th style='font-size:large;'>", "\n") ] for k, v in mapping: my_string = my_string.replace(k, v) fb.clipboard.setText(my_string) user_dirs_str = "" if dirs.USER_DIRS: for d in dirs.USER_DIRS: user_dirs_str += d + '<br />' info_string = ("<b><a href=https://www.github.com/chipmuenk/pyfda>pyfda</a> " "Version {0} (c) 2013 - 2020 Christian Münker</b><br />" "Design, analyze and synthesize digital filters. Docs @ " "<a href=https://pyfda.rtfd.org>pyfda.rtfd.org</a>" " (<a href=https://media.readthedocs.org/pdf/pyfda/latest/pyfda.pdf>pdf</a>)<hr>"\ .format(version.__version__)) versions_string = ( "<b>OS:</b> {0} {1}<br><b>User Name:</b> {2}<br>".format( dirs.OS, dirs.OS_VER, dirs.USER_NAME)) # dir_string = ("<table><th style='font-size:large;'>Imported Modules</th>" # "<tr><td>  {0}</td></tr>"\ # .format( pyfda_lib.mod_version().replace("\n", "<br>  "))) dir_string = ( "<table><th style='font-size:large;'>Software Versions</th>") dir_string += pyfda_lib.mod_version() dir_string += "</table>" dir_string += ("<table><th style='font-size:large;'>Directories</th>" "<tr><td><b>Home:</b></td><td>{0}</td></tr>" "<tr><td><b>Install: </b></td><td>{1}</td></tr>" "<tr><td><b>Config: </b></td><td>{2}</td></tr>" "<tr><td><b>User: </b></td><td>{3}</td></tr>" "<tr><td><b>Temp:</b></td><td>{4}</td></tr>"\ .format( dirs.HOME_DIR, dirs.INSTALL_DIR, dirs.CONF_DIR, user_dirs_str[:-6], dirs.TEMP_DIR)) dir_string += ("<th style='font-size:large;'>Logging Files</th>" "<tr><td><b>Config:</b></td><td>{0}</td></tr>" "<tr><td><b>Output: </b></td><td>{1}</td></tr>" "</table>"\ .format(dirs.USER_LOG_CONF_DIR_FILE, dirs.LOG_DIR_FILE)) about_string = info_string + versions_string + dir_string #msg = QMessageBox.about(self, "About pyFDA", info_string) butClipboard = QPushButton(self) butClipboard.setIcon(QIcon(':/clipboard.svg')) butClipboard.setToolTip("Copy text to clipboard.") # butClipboard.adjustSize() # butClipboard.setFixedSize(self.checkLayout.sizeHint()) msg = QMessageBox(self) msg.setIconPixmap( QPixmap(':/pyfda_icon.svg').scaledToHeight( 32, Qt.SmoothTransformation)) msg.addButton(butClipboard, QMessageBox.ActionRole) msg.setText(about_string) # msg.setInformativeText("This is additional information") #msg.setDetailedText(versions_string) # adds a button that opens another textwindow msg.setWindowTitle("About pyFDA") msg.setStandardButtons(QMessageBox.Ok) # | QMessageBox.Cancel # close Message box with close event triggered by "x" icon msg.closeEvent = self.closeEvent butClipboard.clicked.connect(lambda: to_clipboard(about_string)) retval = msg.exec_()