class FreqUnits(QWidget): """ Build and update widget for entering frequency unit, frequency range and sampling frequency f_S The following key-value pairs of the `fb.fil[0]` dict are modified: - `'freq_specs_unit'` : The unit ('k', 'f_S', 'f_Ny', 'Hz' etc.) as a string - `'freqSpecsRange'` : A list with two entries for minimum and maximum frequency values for labelling the frequency axis - `'f_S'` : The sampling frequency for referring frequency values to as a float - `'f_max'` : maximum frequency for scaling frequency axis - `'plt_fUnit'`: frequency unit as string - `'plt_tUnit'`: time unit as string - `'plt_fLabel'`: label for frequency axis - `'plt_tLabel'`: label for time axis """ # class variables (shared between instances if more than one exists) sig_tx = pyqtSignal(object) # outgoing from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent=None, title="Frequency Units"): super(FreqUnits, self).__init__(parent) self.title = title self.spec_edited = False # flag whether QLineEdit field has been edited self._construct_UI() def _construct_UI(self): """ Construct the User Interface """ self.layVMain = QVBoxLayout() # Widget main layout f_units = ['k', 'f_S', 'f_Ny', 'Hz', 'kHz', 'MHz', 'GHz'] self.t_units = ['', 'T_S', 'T_S', 's', 'ms', r'$\mu$s', 'ns'] bfont = QFont() bfont.setBold(True) self.lblUnits = QLabel(self) self.lblUnits.setText("Freq. Unit") self.lblUnits.setFont(bfont) self.fs_old = fb.fil[0]['f_S'] # store current sampling frequency self.lblF_S = QLabel(self) self.lblF_S.setText(to_html("f_S =", frmt='bi')) self.ledF_S = QLineEdit() self.ledF_S.setText(str(fb.fil[0]["f_S"])) self.ledF_S.setObjectName("f_S") self.ledF_S.installEventFilter(self) # filter events self.butLock = QToolButton(self) self.butLock.setIcon(QIcon(':/lock-unlocked.svg')) self.butLock.setCheckable(True) self.butLock.setChecked(False) self.butLock.setToolTip( "<span><b>Unlocked:</b> When f_S is changed, all frequency related " "widgets are updated, normalized frequencies stay the same.<br />" "<b>Locked:</b> When f_S is changed, displayed absolute frequency " "values don't change but normalized frequencies do.</span>") # self.butLock.setStyleSheet("QToolButton:checked {font-weight:bold}") layHF_S = QHBoxLayout() layHF_S.addWidget(self.ledF_S) layHF_S.addWidget(self.butLock) self.cmbUnits = QComboBox(self) self.cmbUnits.setObjectName("cmbUnits") self.cmbUnits.addItems(f_units) self.cmbUnits.setToolTip( 'Select whether frequencies are specified w.r.t. \n' 'the sampling frequency "f_S", to the Nyquist frequency \n' 'f_Ny = f_S/2 or as absolute values. "k" specifies frequencies w.r.t. f_S ' 'but plots graphs over the frequency index k.') self.cmbUnits.setCurrentIndex(1) # self.cmbUnits.setItemData(0, (0,QColor("#FF333D"),Qt.BackgroundColorRole))# # self.cmbUnits.setItemData(0, (QFont('Verdana', bold=True), Qt.FontRole) fRanges = [("0...½", "half"), ("0...1", "whole"), ("-½...½", "sym")] self.cmbFRange = QComboBox(self) self.cmbFRange.setObjectName("cmbFRange") for f in fRanges: self.cmbFRange.addItem(f[0], f[1]) self.cmbFRange.setToolTip("Select frequency range (whole or half).") self.cmbFRange.setCurrentIndex(0) # Combobox resizes with longest entry self.cmbUnits.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmbFRange.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.butSort = QToolButton(self) self.butSort.setText("Sort") self.butSort.setIcon(QIcon(':/sort-ascending.svg')) #self.butDelCells.setIconSize(q_icon_size) self.butSort.setCheckable(True) self.butSort.setChecked(True) self.butSort.setToolTip( "Sort frequencies in ascending order when pushed.") self.butSort.setStyleSheet("QToolButton:checked {font-weight:bold}") self.layHUnits = QHBoxLayout() self.layHUnits.addWidget(self.cmbUnits) self.layHUnits.addWidget(self.cmbFRange) self.layHUnits.addWidget(self.butSort) # Create a gridLayout consisting of QLabel and QLineEdit fields # for setting f_S, the units and the actual frequency specs: self.layGSpecWdg = QGridLayout() # sublayout for spec fields self.layGSpecWdg.addWidget(self.lblF_S, 1, 0) # self.layGSpecWdg.addWidget(self.ledF_S,1,1) self.layGSpecWdg.addLayout(layHF_S, 1, 1) self.layGSpecWdg.addWidget(self.lblUnits, 0, 0) self.layGSpecWdg.addLayout(self.layHUnits, 0, 1) frmMain = QFrame(self) frmMain.setLayout(self.layGSpecWdg) self.layVMain.addWidget(frmMain) self.layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(self.layVMain) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.cmbUnits.currentIndexChanged.connect(self.update_UI) self.butLock.clicked.connect(self._lock_freqs) self.cmbFRange.currentIndexChanged.connect(self._freq_range) self.butSort.clicked.connect(self._store_sort_flag) # ---------------------------------------------------------------------- self.update_UI() # first-time initialization # ------------------------------------------------------------- def _lock_freqs(self): """ Lock / unlock frequency entries: The values of frequency related widgets are stored in normalized form (w.r.t. sampling frequency)`fb.fil[0]['f_S']`. When the sampling frequency changes, absolute frequencies displayed in the widgets change their values. Most of the time, this is the desired behaviour, the properties of discrete time systems or signals are usually defined by the normalized frequencies. When the effect of varying the sampling frequency is to be analyzed, the displayed values in the widgets can be locked by pressing the Lock button. After changing the sampling frequency, normalized frequencies have to be rescaled like `f_a *= fb.fil[0]['f_S_prev'] / fb.fil[0]['f_S']` to maintain the displayed value `f_a * f_S`. This has to be accomplished by each frequency widget (currently, these are freq_specs and freq_units). The setting is stored as bool in the global dict entry `fb.fil[0]['freq_locked'`, the signal 'view_changed':'f_S' is emitted. """ if self.butLock.isChecked(): # Lock has been activated, keep displayed frequencies locked fb.fil[0]['freq_locked'] = True self.butLock.setIcon(QIcon(':/lock-locked.svg')) else: # Lock has been unlocked, scale displayed frequencies with f_S fb.fil[0]['freq_locked'] = False self.butLock.setIcon(QIcon(':/lock-unlocked.svg')) self.emit({'view_changed': 'f_S'}) # ------------------------------------------------------------- def update_UI(self): """ update_UI is called - during init - when the unit combobox is changed Set various scale factors and labels depending on the setting of the unit combobox. Update the freqSpecsRange and finally, emit 'view_changed':'f_S' signal """ f_unit = str(self.cmbUnits.currentText()) # selected frequency unit idx = self.cmbUnits.currentIndex() # and its index is_normalized_freq = f_unit in {"f_S", "f_Ny", "k"} self.ledF_S.setVisible(not is_normalized_freq) # only vis. when self.lblF_S.setVisible(not is_normalized_freq) # not normalized self.butLock.setVisible(not is_normalized_freq) f_S_scale = 1 # default setting for f_S scale if is_normalized_freq: # store current sampling frequency to restore it when returning to # unnormalized frequencies self.fs_old = fb.fil[0]['f_S'] if f_unit == "f_S": # normalized to f_S fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 1. fb.fil[0]['T_S'] = 1. f_label = r"$F = f\, /\, f_S = \Omega \, /\, 2 \mathrm{\pi} \; \rightarrow$" t_label = r"$n = t\, /\, T_S \; \rightarrow$" elif f_unit == "f_Ny": # normalized to f_nyq = f_S / 2 fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 2. fb.fil[0]['T_S'] = 1. f_label = r"$F = 2f \, / \, f_S = \Omega \, / \, \mathrm{\pi} \; \rightarrow$" t_label = r"$n = t\, /\, T_S \; \rightarrow$" else: # frequency index k, fb.fil[0]['f_S'] = 1. fb.fil[0]['T_S'] = 1. fb.fil[0]['f_max'] = params['N_FFT'] f_label = r"$k \; \rightarrow$" t_label = r"$n\; \rightarrow$" self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S'])) else: # Hz, kHz, ... # Restore sampling frequency when returning from f_S / f_Ny / k if fb.fil[0]['freq_specs_unit'] in { "f_S", "f_Ny", "k" }: # previous setting normalized? fb.fil[0]['f_S'] = fb.fil[0][ 'f_max'] = self.fs_old # yes, restore prev. fb.fil[0][ 'T_S'] = 1. / self.fs_old # settings for sampling frequency self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S'])) if f_unit == "Hz": f_S_scale = 1. elif f_unit == "kHz": f_S_scale = 1.e3 elif f_unit == "MHz": f_S_scale = 1.e6 elif f_unit == "GHz": f_S_scale = 1.e9 else: logger.warning("Unknown frequency unit {0}".format(f_unit)) f_label = r"$f$ in " + f_unit + r"$\; \rightarrow$" t_label = r"$t$ in " + self.t_units[idx] + r"$\; \rightarrow$" if f_unit == "k": plt_f_unit = "f_S" else: plt_f_unit = f_unit fb.fil[0].update({'f_S_scale': f_S_scale}) # scale factor for f_S (Hz, kHz, ...) fb.fil[0].update({'freq_specs_unit': f_unit}) # frequency unit # time and frequency unit as string e.g. for plot axis labeling fb.fil[0].update({"plt_fUnit": plt_f_unit}) fb.fil[0].update({"plt_tUnit": self.t_units[idx]}) # complete plot axis labels including unit and arrow fb.fil[0].update({"plt_fLabel": f_label}) fb.fil[0].update({"plt_tLabel": t_label}) self._freq_range( emit=False) # update f_lim setting without emitting signal self.emit({'view_changed': 'f_S'}) # ------------------------------------------------------------------------------ def eventFilter(self, source, event): """ Filter all events generated by the QLineEdit `f_S` widget. Source and type of all events generated by monitored objects are passed to this eventFilter, evaluated and passed on to the next hierarchy level. - When a QLineEdit widget gains input focus (QEvent.FocusIn`), display the stored value from filter dict with full precision - When a key is pressed inside the text field, set the `spec_edited` flag to True. - When a QLineEdit widget loses input focus (QEvent.FocusOut`), store current value with full precision (only if `spec_edited`== True) and display the stored value in selected format. Emit 'view_changed':'f_S' """ def _store_entry(): """ Update filter dictionary, set line edit entry with reduced precision again. """ if self.spec_edited: fb.fil[0].update({'f_S_prev': fb.fil[0]['f_S']}) fb.fil[0].update({ 'f_S': safe_eval(source.text(), fb.fil[0]['f_S'], sign='pos') }) fb.fil[0].update({'T_S': 1. / fb.fil[0]['f_S']}) fb.fil[0].update({'f_max': fb.fil[0]['f_S']}) self._freq_range(emit=False) # update plotting range self.emit({'view_changed': 'f_S'}) self.spec_edited = False # reset flag, changed entry has been saved if source.objectName() == 'f_S': if event.type() == QEvent.FocusIn: self.spec_edited = False source.setText(str(fb.fil[0]['f_S'])) # full precision elif event.type() == QEvent.KeyPress: self.spec_edited = True # entry has been changed key = event.key() if key in {QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter}: _store_entry() elif key == QtCore.Qt.Key_Escape: # revert changes self.spec_edited = False source.setText(str(fb.fil[0]['f_S'])) # full precision elif event.type() == QEvent.FocusOut: _store_entry() source.setText(params['FMT'].format( fb.fil[0]['f_S'])) # reduced prec. # Call base class method to continue normal event processing: return super(FreqUnits, self).eventFilter(source, event) # ------------------------------------------------------------- def _freq_range(self, emit=True): """ Set frequency plotting range for single-sided spectrum up to f_S/2 or f_S or for double-sided spectrum between -f_S/2 and f_S/2 Emit 'view_changed':'f_range' when `emit=True` """ if type(emit) == int: # signal was emitted by combobox emit = True rangeType = qget_cmb_box(self.cmbFRange) fb.fil[0].update({'freqSpecsRangeType': rangeType}) f_max = fb.fil[0]["f_max"] if rangeType == 'whole': f_lim = [0, f_max] elif rangeType == 'sym': f_lim = [-f_max / 2., f_max / 2.] else: f_lim = [0, f_max / 2.] fb.fil[0]['freqSpecsRange'] = f_lim # store settings in dict if emit: self.emit({'view_changed': 'f_range'}) # ------------------------------------------------------------- def load_dict(self): """ Reload comboBox settings and textfields from filter dictionary Block signals during update of combobox / lineedit widgets """ self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S'])) self.cmbUnits.blockSignals(True) idx = self.cmbUnits.findText( fb.fil[0]['freq_specs_unit']) # get and set self.cmbUnits.setCurrentIndex(idx) # index for freq. unit combo box self.cmbUnits.blockSignals(False) self.cmbFRange.blockSignals(True) idx = self.cmbFRange.findData(fb.fil[0]['freqSpecsRangeType']) self.cmbFRange.setCurrentIndex(idx) # set frequency range self.cmbFRange.blockSignals(False) self.butSort.blockSignals(True) self.butSort.setChecked(fb.fil[0]['freq_specs_sort']) self.butSort.blockSignals(False) # ------------------------------------------------------------- def _store_sort_flag(self): """ Store sort flag in filter dict and emit 'specs_changed':'f_sort' when sort button is checked. """ fb.fil[0]['freq_specs_sort'] = self.butSort.isChecked() if self.butSort.isChecked(): self.emit({'specs_changed': 'f_sort'})
class FreqUnits(QWidget): """ Build and update widget for entering the frequency units The following key-value pairs of the `fb.fil[0]` dict are modified: - `'freq_specs_unit'` : The unit ('k', 'f_S', 'f_Ny', 'Hz' etc.) as a string - `'freqSpecsRange'` : A list with two entries for minimum and maximum frequency values for labelling the frequency axis - `'f_S'` : The sampling frequency for referring frequency values to as a float - `'f_max'` : maximum frequency for scaling frequency axis - `'plt_fUnit'`: frequency unit as string - `'plt_tUnit'`: time unit as string - `'plt_fLabel'`: label for frequency axis - `'plt_tLabel'`: label for time axis """ # class variables (shared between instances if more than one exists) sig_tx = pyqtSignal(object) # outgoing def __init__(self, parent, title="Frequency Units"): super(FreqUnits, self).__init__(parent) self.title = title self.spec_edited = False # flag whether QLineEdit field has been edited self._construct_UI() def _construct_UI(self): """ Construct the User Interface """ self.layVMain = QVBoxLayout() # Widget main layout f_units = ['k', 'f_S', 'f_Ny', 'Hz', 'kHz', 'MHz', 'GHz'] self.t_units = ['', '', '', 's', 'ms', r'$\mu$s', 'ns'] bfont = QFont() bfont.setBold(True) self.lblUnits = QLabel(self) self.lblUnits.setText("Freq. Unit:") self.lblUnits.setFont(bfont) self.fs_old = fb.fil[0]['f_S'] # store current sampling frequency self.ledF_S = QLineEdit() self.ledF_S.setText(str(fb.fil[0]["f_S"])) self.ledF_S.setObjectName("f_S") self.ledF_S.installEventFilter(self) # filter events self.lblF_S = QLabel(self) self.lblF_S.setText(to_html("f_S", frmt='bi')) self.cmbUnits = QComboBox(self) self.cmbUnits.setObjectName("cmbUnits") self.cmbUnits.addItems(f_units) self.cmbUnits.setToolTip( 'Select whether frequencies are specified w.r.t. \n' 'the sampling frequency "f_S", to the Nyquist frequency \n' 'f_Ny = f_S/2 or as absolute values. "k" specifies frequencies w.r.t. f_S ' 'but plots graphs over the frequency index k.') self.cmbUnits.setCurrentIndex(1) # self.cmbUnits.setItemData(0, (0,QColor("#FF333D"),Qt.BackgroundColorRole))# # self.cmbUnits.setItemData(0, (QFont('Verdana', bold=True), Qt.FontRole) fRanges = [("0...½", "half"), ("0...1", "whole"), ("-½...½", "sym")] self.cmbFRange = QComboBox(self) self.cmbFRange.setObjectName("cmbFRange") for f in fRanges: self.cmbFRange.addItem(f[0], f[1]) self.cmbFRange.setToolTip("Select frequency range (whole or half).") self.cmbFRange.setCurrentIndex(0) # Combobox resizes with longest entry self.cmbUnits.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmbFRange.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.butSort = QToolButton(self) self.butSort.setText("Sort") self.butSort.setCheckable(True) self.butSort.setChecked(True) self.butSort.setToolTip( "Sort frequencies in ascending order when pushed.") self.butSort.setStyleSheet("QToolButton:checked {font-weight:bold}") self.layHUnits = QHBoxLayout() self.layHUnits.addWidget(self.cmbUnits) self.layHUnits.addWidget(self.cmbFRange) self.layHUnits.addWidget(self.butSort) # Create a gridLayout consisting of QLabel and QLineEdit fields # for setting f_S, the units and the actual frequency specs: self.layGSpecWdg = QGridLayout() # sublayout for spec fields self.layGSpecWdg.addWidget(self.lblF_S, 1, 0) self.layGSpecWdg.addWidget(self.ledF_S, 1, 1) self.layGSpecWdg.addWidget(self.lblUnits, 0, 0) self.layGSpecWdg.addLayout(self.layHUnits, 0, 1) frmMain = QFrame(self) frmMain.setLayout(self.layGSpecWdg) self.layVMain.addWidget(frmMain) self.layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(self.layVMain) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.cmbUnits.currentIndexChanged.connect(self.update_UI) self.cmbFRange.currentIndexChanged.connect(self._freq_range) self.butSort.clicked.connect(self._store_sort_flag) #---------------------------------------------------------------------- self.update_UI() # first-time initialization #------------------------------------------------------------- def update_UI(self): """ Transform the displayed frequency spec input fields according to the units setting. Spec entries are always stored normalized w.r.t. f_S in the dictionary; when f_S or the unit are changed, only the displayed values of the frequency entries are updated, not the dictionary! Signals are blocked before changing the value for f_S programmatically update_UI is called - during init - when the unit combobox is changed Finally, store freqSpecsRange and emit 'view_changed' signal via _freq_range """ idx = self.cmbUnits.currentIndex() # read index of units combobox f_unit = str(self.cmbUnits.currentText()) # and the label self.ledF_S.setVisible(f_unit not in {"f_S", "f_Ny", "k"}) # only vis. when self.lblF_S.setVisible(f_unit not in {"f_S", "f_Ny", "k"}) # not normalized f_S_scale = 1 # default setting for f_S scale if f_unit in {"f_S", "f_Ny", "k"}: # normalized frequency self.fs_old = fb.fil[0]['f_S'] # store current sampling frequency if f_unit == "f_S": # normalized to f_S fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 1. f_label = r"$F = f\, /\, f_S = \Omega \, /\, 2 \mathrm{\pi} \; \rightarrow$" elif f_unit == "f_Ny": # idx == 1: normalized to f_nyq = f_S / 2 fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 2. f_label = r"$F = 2f \, / \, f_S = \Omega \, / \, \mathrm{\pi} \; \rightarrow$" else: fb.fil[0]['f_S'] = 1 fb.fil[0]['f_max'] = params['N_FFT'] f_label = r"$k \; \rightarrow$" t_label = r"$n \; \rightarrow$" self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S'])) else: # Hz, kHz, ... if fb.fil[0]['freq_specs_unit'] in {"f_S", "f_Ny", "k"}: # previous setting fb.fil[0]['f_S'] = fb.fil[0][ 'f_max'] = self.fs_old # restore prev. sampling frequency self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S'])) if f_unit == "Hz": f_S_scale = 1. elif f_unit == "kHz": f_S_scale = 1.e3 elif f_unit == "MHz": f_S_scale = 1.e6 elif f_unit == "GHz": f_S_scale = 1.e9 else: logger.warning("Unknown frequency unit {0}".format(f_unit)) f_label = r"$f$ in " + f_unit + r"$\; \rightarrow$" t_label = r"$t$ in " + self.t_units[idx] + r"$\; \rightarrow$" if f_unit == "k": plt_f_unit = "f_S" else: plt_f_unit = f_unit fb.fil[0].update({'f_S_scale': f_S_scale}) # scale factor for f_S fb.fil[0].update({'freq_specs_unit': f_unit}) # frequency unit fb.fil[0].update({"plt_fLabel": f_label}) # label for freq. axis fb.fil[0].update({"plt_tLabel": t_label}) # label for time axis fb.fil[0].update({"plt_fUnit": plt_f_unit}) # frequency unit as string fb.fil[0].update({"plt_tUnit": self.t_units[idx]}) # time unit as string self._freq_range( ) # update f_lim setting and emit sigUnitChanged signal #------------------------------------------------------------------------------ def eventFilter(self, source, event): """ Filter all events generated by the QLineEdit widgets. Source and type of all events generated by monitored objects are passed to this eventFilter, evaluated and passed on to the next hierarchy level. - When a QLineEdit widget gains input focus (QEvent.FocusIn`), display the stored value from filter dict with full precision - When a key is pressed inside the text field, set the `spec_edited` flag to True. - When a QLineEdit widget loses input focus (QEvent.FocusOut`), store current value with full precision (only if `spec_edited`== True) and display the stored value in selected format. Emit 'view_changed':'f_S' """ def _store_entry(): """ Update filter dictionary, set line edit entry with reduced precision again. """ if self.spec_edited: fb.fil[0].update({ 'f_S': safe_eval(source.text(), fb.fil[0]['f_S'], sign='pos') }) # TODO: ?! self._freq_range(emit_sig_range=False) # update plotting range self.sig_tx.emit({'sender': __name__, 'view_changed': 'f_S'}) self.spec_edited = False # reset flag, changed entry has been saved if source.objectName() == 'f_S': if event.type() == QEvent.FocusIn: self.spec_edited = False source.setText(str(fb.fil[0]['f_S'])) # full precision elif event.type() == QEvent.KeyPress: self.spec_edited = True # entry has been changed key = event.key() if key in {QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter}: _store_entry() elif key == QtCore.Qt.Key_Escape: # revert changes self.spec_edited = False source.setText(str(fb.fil[0]['f_S'])) # full precision elif event.type() == QEvent.FocusOut: _store_entry() source.setText(params['FMT'].format( fb.fil[0]['f_S'])) # reduced precision # Call base class method to continue normal event processing: return super(FreqUnits, self).eventFilter(source, event) #------------------------------------------------------------- def _freq_range(self, emit_sig_range=True): """ Set frequency plotting range for single-sided spectrum up to f_S/2 or f_S or for double-sided spectrum between -f_S/2 and f_S/2 and emit 'view_changed':'f_range'. """ rangeType = qget_cmb_box(self.cmbFRange) fb.fil[0].update({'freqSpecsRangeType': rangeType}) f_max = fb.fil[0]["f_max"] if rangeType == 'whole': f_lim = [0, f_max] elif rangeType == 'sym': f_lim = [-f_max / 2., f_max / 2.] else: f_lim = [0, f_max / 2.] fb.fil[0]['freqSpecsRange'] = f_lim # store settings in dict self.sig_tx.emit({'sender': __name__, 'view_changed': 'f_range'}) #------------------------------------------------------------- def load_dict(self): """ Reload comboBox settings and textfields from filter dictionary Block signals during update of combobox / lineedit widgets """ self.ledF_S.setText(params['FMT'].format(fb.fil[0]['f_S'])) self.cmbUnits.blockSignals(True) idx = self.cmbUnits.findText( fb.fil[0]['freq_specs_unit']) # get and set self.cmbUnits.setCurrentIndex(idx) # index for freq. unit combo box self.cmbUnits.blockSignals(False) self.cmbFRange.blockSignals(True) idx = self.cmbFRange.findData(fb.fil[0]['freqSpecsRangeType']) self.cmbFRange.setCurrentIndex(idx) # set frequency range self.cmbFRange.blockSignals(False) self.butSort.blockSignals(True) self.butSort.setChecked(fb.fil[0]['freq_specs_sort']) self.butSort.blockSignals(False) #------------------------------------------------------------- def _store_sort_flag(self): """ Store sort flag in filter dict and emit 'specs_changed':'f_sort' when sort button is checked. """ fb.fil[0]['freq_specs_sort'] = self.butSort.isChecked() if self.butSort.isChecked(): self.sig_tx.emit({'sender': __name__, 'specs_changed': 'f_sort'})
class AmplitudeSpecs(QWidget): """ Build and update widget for entering the amplitude specifications like A_SB, A_PB etc. """ sig_tx = pyqtSignal( object) # emitted when amplitude unit or spec has been changed def __init__(self, parent, title="Amplitude Specs"): """ Initialize """ super(AmplitudeSpecs, self).__init__(parent) self.title = title self.qlabels = [] # list with references to QLabel widgets self.qlineedit = [] # list with references to QLineEdit widgets self.spec_edited = False # flag whether QLineEdit field has been edited self._construct_UI() #------------------------------------------------------------------------------ def _construct_UI(self): """ Construct User Interface """ amp_units = ["dB", "V", "W"] bfont = QFont() bfont.setBold(True) lblTitle = QLabel(str(self.title), self) # field for widget title lblTitle.setFont(bfont) lblTitle.setWordWrap(True) lblUnits = QLabel("in", self) self.cmbUnitsA = QComboBox(self) self.cmbUnitsA.addItems(amp_units) self.cmbUnitsA.setObjectName("cmbUnitsA") self.cmbUnitsA.setToolTip( "<span>Unit for amplitude specifications:" " dB is attenuation (> 0); levels in V and W have to be < 1.</span>" ) # fit size dynamically to largest element: self.cmbUnitsA.setSizeAdjustPolicy(QComboBox.AdjustToContents) # find index for default unit from dictionary and set the unit amp_idx = self.cmbUnitsA.findData(fb.fil[0]['amp_specs_unit']) if amp_idx < 0: amp_idx = 0 self.cmbUnitsA.setCurrentIndex(amp_idx) # initialize for dBs layHTitle = QHBoxLayout() # layout for title and unit layHTitle.addWidget(lblTitle) layHTitle.addWidget(lblUnits, Qt.AlignLeft) layHTitle.addWidget(self.cmbUnitsA, Qt.AlignLeft) layHTitle.addStretch(1) self.layGSpecs = QGridLayout() # sublayout for spec fields # set the title as the first (fixed) entry in grid layout. The other # fields are added and hidden dynamically in _show_entries and _hide_entries() self.layGSpecs.addLayout(layHTitle, 0, 0, 1, 2) self.layGSpecs.setAlignment(Qt.AlignLeft) # This is the top level widget, encompassing the other widgets self.frmMain = QFrame(self) self.frmMain.setLayout(self.layGSpecs) self.layVMain = QVBoxLayout() # Widget main layout self.layVMain.addWidget(self.frmMain) self.layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(self.layVMain) self.n_cur_labels = 0 # number of currently visible labels / qlineedits # - Build a list from all entries in the fil_dict dictionary starting # with "A" (= amplitude specifications of the current filter) # - Pass the list to update_UI which recreates the widget # ATTENTION: Entries need to be converted from QString to str for Py 2 new_labels = [str(l) for l in fb.fil[0] if l[0] == 'A'] self.update_UI(new_labels=new_labels) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs / EVENT MONITORING #---------------------------------------------------------------------- self.cmbUnitsA.currentIndexChanged.connect(self._set_amp_unit) # ^ this also triggers the initial load_dict # DYNAMIC EVENT MONITORING # Every time a field is edited, call self._store_entry and # self.load_dict. This is achieved by dynamically installing and # removing event filters when creating / deleting subwidgets. # The event filter monitors the focus of the input fields. #------------------------------------------------------------------------------ def eventFilter(self, source, event): """ Filter all events generated by the QLineEdit widgets. Source and type of all events generated by monitored objects are passed to this eventFilter, evaluated and passed on to the next hierarchy level. - When a QLineEdit widget gains input focus (QEvent.FocusIn`), display the stored value from filter dict with full precision - When a key is pressed inside the text field, set the `spec_edited` flag to True. - When a QLineEdit widget loses input focus (QEvent.FocusOut`), store current value in linear format with full precision (only if `spec_edited`== True) and display the stored value in selected format """ if isinstance(source, QLineEdit): # could be extended for other widgets if event.type() == QEvent.FocusIn: self.spec_edited = False self.load_dict() # store current entry in case new value can't be evaluated: fb.data_old = source.text() elif event.type() == QEvent.KeyPress: self.spec_edited = True # entry has been changed key = event.key() if key in {QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter}: # store entry self._store_entry(source) elif key == QtCore.Qt.Key_Escape: # revert changes self.spec_edited = False self.load_dict() elif event.type() == QEvent.FocusOut: self._store_entry(source) # Call base class method to continue normal event processing: return super(AmplitudeSpecs, self).eventFilter(source, event) #------------------------------------------------------------- def update_UI(self, new_labels=()): """ Called from filter_specs.update_UI() and target_specs.update_UI(). Set labels and get corresponding values from filter dictionary. When number of entries has changed, the layout of subwidget is rebuilt, using - `self.qlabels`, a list with references to existing QLabel widgets, - `new_labels`, a list of strings from the filter_dict for the current filter design - 'num_new_labels`, their number - `self.n_cur_labels`, the number of currently visible labels / qlineedit fields """ state = new_labels[0] new_labels = new_labels[1:] # W_lbl = max([self.qfm.width(l) for l in new_labels]) # max. label width in pixel num_new_labels = len(new_labels) if num_new_labels < self.n_cur_labels: # less new labels/qlineedit fields than before self._hide_entries(num_new_labels) elif num_new_labels > self.n_cur_labels: # more new labels, create / show new ones self._show_entries(num_new_labels) tool_tipp_sb = "Min. attenuation resp. maximum level in (this) stop band" for i in range(num_new_labels): # Update ALL labels and corresponding values self.qlabels[i].setText(to_html(new_labels[i], frmt='bi')) self.qlineedit[i].setText(str(fb.fil[0][new_labels[i]])) self.qlineedit[i].setObjectName(new_labels[i]) # update ID if "sb" in new_labels[i].lower(): self.qlineedit[i].setToolTip("<span>" + tool_tipp_sb + " (> 0).</span>") elif "pb" in new_labels[i].lower(): self.qlineedit[i].setToolTip( "<span>Maximum ripple (> 0) in (this) pass band.<span/>" ) qstyle_widget(self.qlineedit[i], state) self.n_cur_labels = num_new_labels # update number of currently visible labels self.load_dict( ) # display rounded filter dict entries in selected unit #------------------------------------------------------------------------------ def load_dict(self): """ Reload and reformat the amplitude textfields from filter dict when a new filter design algorithm is selected or when the user has changed the unit (V / W / dB): - Reload amplitude entries from filter dictionary and convert to selected to reflect changed settings unit. - Update the lineedit fields, rounded to specified format. """ unit = fb.fil[0]['amp_specs_unit'] filt_type = fb.fil[0]['ft'] for i in range(len(self.qlineedit)): amp_label = str(self.qlineedit[i].objectName()) amp_value = lin2unit(fb.fil[0][amp_label], filt_type, amp_label, unit=unit) if not self.qlineedit[i].hasFocus(): # widget has no focus, round the display self.qlineedit[i].setText(params['FMT'].format(amp_value)) else: # widget has focus, show full precision self.qlineedit[i].setText(str(amp_value)) #------------------------------------------------------------------------------ def _set_amp_unit(self, source): """ Store unit for amplitude in filter dictionary, reload amplitude spec entries via load_dict and fire a sigUnitChanged signal """ fb.fil[0]['amp_specs_unit'] = qget_cmb_box(self.cmbUnitsA, data=False) self.load_dict() self.sig_tx.emit({'sender': __name__, 'view_changed': 'a_unit'}) #------------------------------------------------------------------------------ def _store_entry(self, source): """ When the textfield of `source` has been edited (flag `self.spec_edited` = True), transform the amplitude spec back to linear unit setting and store it in filter dict. This is triggered by `QEvent.focusOut` Spec entries are *always* stored in linear units; only the displayed values are adapted to the amplitude unit, not the dictionary! """ if self.spec_edited: unit = str(self.cmbUnitsA.currentText()) filt_type = fb.fil[0]['ft'] amp_label = str(source.objectName()) amp_value = safe_eval(source.text(), fb.data_old, sign='pos') fb.fil[0].update( {amp_label: unit2lin(amp_value, filt_type, amp_label, unit)}) self.sig_tx.emit({'sender': __name__, 'specs_changed': 'a_specs'}) self.spec_edited = False # reset flag self.load_dict() #------------------------------------------------------------- def _hide_entries(self, num_new_labels): """ Hide subwidgets so that only `num_new_labels` subwidgets are visible """ for i in range(num_new_labels, len(self.qlabels)): self.qlabels[i].hide() self.qlineedit[i].hide() #------------------------------------------------------------------------ def _show_entries(self, num_new_labels): """ - check whether enough subwidgets (QLabel und QLineEdit) exist for the the required number of `num_new_labels`: - create new ones if required - initialize them with dummy information - install eventFilter for new QLineEdit widgets so that the filter dict is updated automatically when a QLineEdit field has been edited. - if enough subwidgets exist already, make enough of them visible to show all spec fields """ num_tot_labels = len( self.qlabels) # number of existing labels (vis. + invis.) if num_tot_labels < num_new_labels: # new widgets need to be generated for i in range(num_tot_labels, num_new_labels): self.qlabels.append(QLabel(self)) self.qlabels[i].setText(to_html("dummy", frmt='bi')) self.qlineedit.append(QLineEdit("")) self.qlineedit[i].setObjectName("dummy") self.qlineedit[i].installEventFilter(self) # filter events # first entry is title self.layGSpecs.addWidget(self.qlabels[i], i + 1, 0) self.layGSpecs.addWidget(self.qlineedit[i], i + 1, 1) else: # make the right number of widgets visible for i in range(self.n_cur_labels, num_new_labels): self.qlabels[i].show() self.qlineedit[i].show()
class SelectFilter(QWidget): """ Construct and read combo boxes for selecting the filter, consisting of the following hierarchy: 1. Response Type rt (LP, HP, Hilbert, ...) 2. Filter Type ft (IIR, FIR, CIC ...) 3. Filter Class (Butterworth, ...) Every time a combo box is changed manually, the filter tree for the selected response resp. filter type is read and the combo box(es) further down in the hierarchy are populated according to the available combinations. sig_tx({'filt_changed'}) is emitted and propagated to input_filter_specs.py where it triggers the recreation of all subwidgets. """ sig_tx = pyqtSignal(object) # outgoing from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent=None): super(SelectFilter, self).__init__(parent) self.fc_last = '' # previous filter class self._construct_UI() self._set_response_type() # first time initialization def _construct_UI(self): """ Construct UI with comboboxes for selecting filter: - cmbResponseType for selecting response type rt (LP, HP, ...) - cmbFilterType for selection of filter type (IIR, FIR, ...) - cmbFilterClass for selection of design design class (Chebyshev, ...) and populate them from the "filterTree" dict during the initial run. Later, calling _set_response_type() updates the three combo boxes. See filterbroker.py for structure and content of "filterTree" dict """ # ---------------------------------------------------------------------- # Combo boxes for filter selection # ---------------------------------------------------------------------- self.cmbResponseType = QComboBox(self) self.cmbResponseType.setObjectName("comboResponseType") self.cmbResponseType.setToolTip("Select filter response type.") self.cmbFilterType = QComboBox(self) self.cmbFilterType.setObjectName("comboFilterType") self.cmbFilterType.setToolTip( "<span>Choose filter type, either recursive (Infinite Impulse Response) " "or transversal (Finite Impulse Response).</span>") self.cmbFilterClass = QComboBox(self) self.cmbFilterClass.setObjectName("comboFilterClass") self.cmbFilterClass.setToolTip("Select the filter design class.") # Adapt comboboxes size dynamically to largest element self.cmbResponseType.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmbFilterType.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmbFilterClass.setSizeAdjustPolicy(QComboBox.AdjustToContents) # ---------------------------------------------------------------------- # Populate combo box with initial settings from fb.fil_tree # ---------------------------------------------------------------------- # Translate short response type ("LP") to displayed names ("Lowpass") # (correspondence is defined in pyfda_rc.py) and populate rt combo box # rt_list = sorted(list(fb.fil_tree.keys())) for rt in rt_list: try: self.cmbResponseType.addItem(rc.rt_names[rt], rt) except KeyError as e: logger.warning( f"KeyError: {e} has no corresponding full name in rc.rt_names." ) idx = self.cmbResponseType.findData('LP') # find index for 'LP' if idx == -1: # Key 'LP' does not exist, use first entry instead idx = 0 self.cmbResponseType.setCurrentIndex(idx) # set initial index rt = qget_cmb_box(self.cmbResponseType) for ft in fb.fil_tree[rt]: self.cmbFilterType.addItem(rc.ft_names[ft], ft) self.cmbFilterType.setCurrentIndex(0) # set initial index ft = qget_cmb_box(self.cmbFilterType) for fc in fb.fil_tree[rt][ft]: self.cmbFilterClass.addItem(fb.filter_classes[fc]['name'], fc) self.cmbFilterClass.setCurrentIndex(0) # set initial index # ---------------------------------------------------------------------- # Layout for Filter Type Subwidgets # ---------------------------------------------------------------------- layHFilWdg = QHBoxLayout() # container for filter subwidgets layHFilWdg.addWidget(self.cmbResponseType) # LP, HP, BP, etc. layHFilWdg.addStretch() layHFilWdg.addWidget(self.cmbFilterType) # FIR, IIR layHFilWdg.addStretch() layHFilWdg.addWidget(self.cmbFilterClass) # bessel, elliptic, etc. # ---------------------------------------------------------------------- # Layout for dynamic filter subwidgets (empty frame) # ---------------------------------------------------------------------- # see Summerfield p. 278 self.layHDynWdg = QHBoxLayout() # for additional dynamic subwidgets # ---------------------------------------------------------------------- # Filter Order Subwidgets # ---------------------------------------------------------------------- self.lblOrder = QLabel("<b>Order:</b>") self.chkMinOrder = QCheckBox("Minimum", self) self.chkMinOrder.setToolTip( "<span>Minimum filter order / # of taps is determined automatically.</span>" ) self.lblOrderN = QLabel("<b><i>N =</i></b>") self.ledOrderN = QLineEdit(str(fb.fil[0]['N']), self) self.ledOrderN.setToolTip("Filter order (# of taps - 1).") # -------------------------------------------------- # Layout for filter order subwidgets # -------------------------------------------------- layHOrdWdg = QHBoxLayout() layHOrdWdg.addWidget(self.lblOrder) layHOrdWdg.addWidget(self.chkMinOrder) layHOrdWdg.addStretch() layHOrdWdg.addWidget(self.lblOrderN) layHOrdWdg.addWidget(self.ledOrderN) # ---------------------------------------------------------------------- # OVERALL LAYOUT (stack standard + dynamic subwidgets vertically) # ---------------------------------------------------------------------- self.layVAllWdg = QVBoxLayout() self.layVAllWdg.addLayout(layHFilWdg) self.layVAllWdg.addLayout(self.layHDynWdg) self.layVAllWdg.addLayout(layHOrdWdg) # ============================================================================== frmMain = QFrame(self) frmMain.setLayout(self.layVAllWdg) layHMain = QHBoxLayout() layHMain.addWidget(frmMain) layHMain.setContentsMargins(*rc.params['wdg_margins']) self.setLayout(layHMain) # ============================================================================== # ------------------------------------------------------------ # SIGNALS & SLOTS # ------------------------------------------------------------ # Connect comboBoxes and setters, propgate change events hierarchically # through all widget methods and emit 'filt_changed' in the end. self.cmbResponseType.currentIndexChanged.connect( lambda: self._set_response_type(enb_signal=True)) # 'LP' self.cmbFilterType.currentIndexChanged.connect( lambda: self._set_filter_type(enb_signal=True)) # 'IIR' self.cmbFilterClass.currentIndexChanged.connect( lambda: self._set_design_method(enb_signal=True)) # 'cheby1' self.chkMinOrder.clicked.connect( lambda: self._set_filter_order(enb_signal=True)) # Min. Order self.ledOrderN.editingFinished.connect( lambda: self._set_filter_order(enb_signal=True)) # Manual Order # ------------------------------------------------------------ # ------------------------------------------------------------------------------ def load_dict(self): """ Reload comboboxes from filter dictionary to update changed settings after loading a filter design from disk. `load_dict` uses the automatism of _set_response_type etc. of checking whether the previously selected filter design method is also available for the new combination. """ # find index for response type: rt_idx = self.cmbResponseType.findData(fb.fil[0]['rt']) self.cmbResponseType.setCurrentIndex(rt_idx) self._set_response_type() # ------------------------------------------------------------------------------ def _set_response_type(self, enb_signal=False): """ Triggered when cmbResponseType (LP, HP, ...) is changed: Copy selection to self.rt and fb.fil[0] and reconstruct filter type combo If previous filter type (FIR, IIR, ...) exists for new rt, set the filter type combo box to the old setting """ # Read current setting of comboBox as string and store it in the filter dict fb.fil[0]['rt'] = self.rt = qget_cmb_box(self.cmbResponseType) # Get list of available filter types for new rt ft_list = list( fb.fil_tree[self.rt].keys()) # explicit list() needed for Py3 # --------------------------------------------------------------- # Rebuild filter type combobox entries for new rt setting self.cmbFilterType.blockSignals( True) # don't fire when changed programmatically self.cmbFilterType.clear() for ft in fb.fil_tree[self.rt]: self.cmbFilterType.addItem(rc.ft_names[ft], ft) # Is current filter type (e.g. IIR) in list for new rt? if fb.fil[0]['ft'] in ft_list: ft_idx = self.cmbFilterType.findText(fb.fil[0]['ft']) self.cmbFilterType.setCurrentIndex( ft_idx) # yes, set same ft as before else: self.cmbFilterType.setCurrentIndex(0) # no, set index 0 self.cmbFilterType.blockSignals(False) # --------------------------------------------------------------- self._set_filter_type(enb_signal) # ------------------------------------------------------------------------------ def _set_filter_type(self, enb_signal=False): """" Triggered when cmbFilterType (IIR, FIR, ...) is changed: - read filter type ft and copy it to fb.fil[0]['ft'] and self.ft - (re)construct design method combo, adding displayed text (e.g. "Chebyshev 1") and hidden data (e.g. "cheby1") """ # Read out current setting of comboBox and convert to string fb.fil[0]['ft'] = self.ft = qget_cmb_box(self.cmbFilterType) # logger.debug("InputFilter.set_filter_type triggered: {0}".format( self.ft)) # --------------------------------------------------------------- # Get all available design methods for new ft from fil_tree and # - Collect them in fc_list # - Rebuild design method combobox entries for new ft setting: # The combobox is populated with the "long name", # the internal name is stored in comboBox.itemData self.cmbFilterClass.blockSignals(True) self.cmbFilterClass.clear() fc_list = [] for fc in sorted(fb.fil_tree[self.rt][self.ft]): self.cmbFilterClass.addItem(fb.filter_classes[fc]['name'], fc) fc_list.append(fc) logger.debug("fc_list: {0}\n{1}".format(fc_list, fb.fil[0]['fc'])) # Does new ft also provide the previous design method (e.g. ellip)? # Has filter been instantiated? if fb.fil[0]['fc'] in fc_list and ff.fil_inst: # yes, set same fc as before fc_idx = self.cmbFilterClass.findText( fb.filter_classes[fb.fil[0]['fc']]['name']) logger.debug("fc_idx : %s", fc_idx) self.cmbFilterClass.setCurrentIndex(fc_idx) else: self.cmbFilterClass.setCurrentIndex(0) # no, set index 0 self.cmbFilterClass.blockSignals(False) self._set_design_method(enb_signal) # ------------------------------------------------------------------------------ def _set_design_method(self, enb_signal=False): """ Triggered when cmbFilterClass (cheby1, ...) is changed: - read design method fc and copy it to fb.fil[0] - create / update global filter instance fb.fil_inst of fc class - update dynamic widgets (if fc has changed and if there are any) - call load filter order """ fb.fil[0]['fc'] = fc = qget_cmb_box(self.cmbFilterClass) if fc != self.fc_last: # fc has changed: # when filter has been changed, try to destroy dynamic widgets of last fc: if self.fc_last: self._destruct_dyn_widgets() # ================================================================== """ Create new instance of the selected filter class, accessible via its handle fb.fil_inst """ err = ff.fil_factory.create_fil_inst(fc) logger.debug(f"InputFilter.set_design_method triggered: {fc}\n" f"Returned error code {err}") # ================================================================== # Check whether new design method also provides the old filter order # method. If yes, don't change it, else set first available # filter order method if fb.fil[0]['fo'] not in fb.fil_tree[self.rt][self.ft][fc].keys(): fb.fil[0].update({'fo': {}}) # explicit list(dict.keys()) needed for Python 3 fb.fil[0]['fo'] = list( fb.fil_tree[self.rt][self.ft][fc].keys())[0] # ============================================================================= # logger.debug("selFilter = %s" # "filterTree[fc] = %s" # "filterTree[fc].keys() = %s" # %(fb.fil[0], fb.fil_tree[self.rt][self.ft][fc],\ # fb.fil_tree[self.rt][self.ft][fc].keys() # )) # # ============================================================================= # construct dyn. subwidgets if available if hasattr(ff.fil_inst, 'construct_UI'): self._construct_dyn_widgets() self.fc_last = fb.fil[0]['fc'] self.load_filter_order(enb_signal) # ------------------------------------------------------------------------------ def load_filter_order(self, enb_signal=False): """ Called by set_design_method or from InputSpecs (with enb_signal = False), load filter order setting from fb.fil[0] and update widgets """ # collect dict_keys of available filter order [fo] methods for selected # design method [fc] from fil_tree (explicit list() needed for Python 3) fo_dict = fb.fil_tree[fb.fil[0]['rt']][fb.fil[0]['ft']][fb.fil[0] ['fc']] fo_list = list(fo_dict.keys()) # is currently selected fo setting available for (new) fc ? if fb.fil[0]['fo'] in fo_list: self.fo = fb.fil[0]['fo'] # keep current setting else: self.fo = fo_list[0] # use first list entry from filterTree fb.fil[0]['fo'] = self.fo # and update fo method # check whether fo widget is active, disabled or invisible if 'fo' in fo_dict[self.fo] and len(fo_dict[self.fo]['fo']) > 1: status = fo_dict[self.fo]['fo'][0] else: status = 'i' # Determine which subwidgets are __visible__ self.chkMinOrder.setVisible('min' in fo_list) self.ledOrderN.setVisible(status in {'a', 'd'}) self.lblOrderN.setVisible(status in {'a', 'd'}) # Determine which subwidgets are __enabled__ self.chkMinOrder.setChecked(fb.fil[0]['fo'] == 'min') self.ledOrderN.setText(str(fb.fil[0]['N'])) self.ledOrderN.setEnabled(not self.chkMinOrder.isChecked() and status == 'a') self.lblOrderN.setEnabled(not self.chkMinOrder.isChecked() and status == 'a') if enb_signal: logger.debug("Emit 'filt_changed'") self.emit({'filt_changed': 'filter_type'}) # ------------------------------------------------------------------------------ def _set_filter_order(self, enb_signal=False): """ Triggered when either ledOrderN or chkMinOrder are edited: - copy settings to fb.fil[0] - emit 'filt_changed' if enb_signal is True """ # Determine which subwidgets are _enabled_ if self.chkMinOrder.isVisible(): self.ledOrderN.setEnabled(not self.chkMinOrder.isChecked()) self.lblOrderN.setEnabled(not self.chkMinOrder.isChecked()) if self.chkMinOrder.isChecked() is True: # update in case N has been changed outside this class self.ledOrderN.setText(str(fb.fil[0]['N'])) fb.fil[0].update({'fo': 'min'}) else: fb.fil[0].update({'fo': 'man'}) else: self.lblOrderN.setEnabled(self.fo == 'man') self.ledOrderN.setEnabled(self.fo == 'man') # read manual filter order, convert to positive integer and store it # in filter dictionary. ordn = safe_eval(self.ledOrderN.text(), fb.fil[0]['N'], return_type='int', sign='pos') ordn = ordn if ordn > 0 else 1 self.ledOrderN.setText(str(ordn)) fb.fil[0].update({'N': ordn}) if enb_signal: logger.debug("Emit 'filt_changed'") self.emit({'filt_changed': 'filter_order_widget'}) # ------------------------------------------------------------------------------ def _destruct_dyn_widgets(self): """ Delete the dynamically created filter design subwidget (if there is one) see http://stackoverflow.com/questions/13827798/proper-way-to-cleanup- widgets-in-pyqt This does NOT work when the subwidgets to be deleted and created are identical, as the deletion is only performed when the current scope has been left (?)! Hence, it is necessary to skip this method when the new design method is the same as the old one. """ if hasattr(ff.fil_inst, 'wdg_fil'): # not needed, connection is destroyed automatically # ff.fil_inst.sig_tx.disconnect() try: # remove widget from layout self.layHDynWdg.removeWidget(self.dyn_wdg_fil) # delete UI widget when scope has been left self.dyn_wdg_fil.deleteLater() except AttributeError as e: logger.error("Could not destruct_UI!\n{0}".format(e)) ff.fil_inst.deleteLater( ) # delete QWidget when scope has been left # ------------------------------------------------------------------------------ def _construct_dyn_widgets(self): """ Create filter widget UI dynamically (if the filter routine has one) and connect its sig_tx signal to sig_tx in this scope. """ ff.fil_inst.construct_UI() if hasattr(ff.fil_inst, 'wdg_fil'): try: self.dyn_wdg_fil = getattr(ff.fil_inst, 'wdg_fil') self.layHDynWdg.addWidget(self.dyn_wdg_fil, stretch=1) except AttributeError as e: logger.warning(e) if hasattr(ff.fil_inst, 'sig_tx'): ff.fil_inst.sig_tx.connect(self.sig_tx)