class WndManageEmissionLines(SecondaryWindow): signal_selected_element_changed = Signal(str) signal_update_element_selection_list = Signal() signal_update_add_remove_btn_state = Signal(bool, bool) signal_marker_state_changed = Signal(bool) signal_parameters_changed = Signal() def __init__(self, *, gpc, gui_vars): super().__init__() # Global processing classes self.gpc = gpc # Global GUI variables (used for control of GUI state) self.gui_vars = gui_vars # Threshold used for peak removal (displayed in lineedit) self._remove_peak_threshold = self.gpc.get_peak_threshold() self._enable_events = False self._eline_list = [ ] # List of emission lines (used in the line selection combo) self._table_contents = [ ] # Keep a copy of table contents (list of dict) self._selected_eline = "" self.initialize() self._enable_events = True # Marker state is reported by Matplotlib plot in 'line_plot' model def cb(marker_state): self.signal_marker_state_changed.emit(marker_state) self.gpc.set_marker_reporter(cb) self.signal_marker_state_changed.connect( self.slot_marker_state_changed) # Update button states self._update_add_remove_btn_state() self._update_add_edit_userpeak_btn_state() self._update_add_edit_pileup_peak_btn_state() def initialize(self): self.setWindowTitle("PyXRF: Add/Remove Emission Lines") self.resize(600, 600) top_buttons = self._setup_select_elines() self._setup_elines_table() bottom_buttons = self._setup_action_buttons() vbox = QVBoxLayout() # Group of buttons above the table vbox.addLayout(top_buttons) # Tables hbox = QHBoxLayout() hbox.addWidget(self.tbl_elines) vbox.addLayout(hbox) vbox.addLayout(bottom_buttons) self.setLayout(vbox) self._set_tooltips() def _setup_select_elines(self): self.cb_select_all = QCheckBox("All") self.cb_select_all.setChecked(True) self.cb_select_all.toggled.connect(self.cb_select_all_toggled) self.element_selection = ElementSelection() # The following field should switched to 'editable' state from when needed self.le_peak_intensity = LineEditReadOnly() self.pb_add_eline = QPushButton("Add") self.pb_add_eline.clicked.connect(self.pb_add_eline_clicked) self.pb_remove_eline = QPushButton("Remove") self.pb_remove_eline.clicked.connect(self.pb_remove_eline_clicked) self.pb_user_peaks = QPushButton("New User Peak ...") self.pb_user_peaks.clicked.connect(self.pb_user_peaks_clicked) self.pb_pileup_peaks = QPushButton("New Pileup Peak ...") self.pb_pileup_peaks.clicked.connect(self.pb_pileup_peaks_clicked) self.element_selection.signal_current_item_changed.connect( self.element_selection_item_changed) vbox = QVBoxLayout() hbox = QHBoxLayout() hbox.addWidget(self.element_selection) hbox.addWidget(self.le_peak_intensity) hbox.addWidget(self.pb_add_eline) hbox.addWidget(self.pb_remove_eline) vbox.addLayout(hbox) hbox = QHBoxLayout() hbox.addWidget(self.cb_select_all) hbox.addStretch(1) hbox.addWidget(self.pb_user_peaks) hbox.addWidget(self.pb_pileup_peaks) vbox.addLayout(hbox) # Wrap vbox into hbox, because it will be inserted into vbox hbox = QHBoxLayout() hbox.addLayout(vbox) hbox.addStretch(1) return hbox def _setup_elines_table(self): """The table has only functionality necessary to demonstrate how it is going to look. A lot more code is needed to actually make it run.""" self._validator_peak_height = QDoubleValidator() self._validator_peak_height.setBottom(0.01) self.tbl_elines = QTableWidget() self.tbl_elines.setStyleSheet( "QTableWidget::item{color: black;}" "QTableWidget::item:selected{background-color: red;}" "QTableWidget::item:selected{color: white;}") self.tbl_labels = [ "", "Z", "Line", "E, keV", "Peak Int.", "Rel. Int.(%)", "CS" ] self.tbl_cols_editable = ["Peak Int."] self.tbl_value_min = {"Rel. Int.(%)": 0.1} tbl_cols_resize_to_content = ["", "Z", "Line"] self.tbl_elines.setColumnCount(len(self.tbl_labels)) self.tbl_elines.verticalHeader().hide() self.tbl_elines.setHorizontalHeaderLabels(self.tbl_labels) self.tbl_elines.setSelectionBehavior(QTableWidget.SelectRows) self.tbl_elines.setSelectionMode(QTableWidget.SingleSelection) self.tbl_elines.itemSelectionChanged.connect( self.tbl_elines_item_selection_changed) self.tbl_elines.itemChanged.connect(self.tbl_elines_item_changed) header = self.tbl_elines.horizontalHeader() for n, lbl in enumerate(self.tbl_labels): # Set stretching for the columns if lbl in tbl_cols_resize_to_content: header.setSectionResizeMode(n, QHeaderView.ResizeToContents) else: header.setSectionResizeMode(n, QHeaderView.Stretch) # Set alignment for the columns headers (HEADERS only) if n == 0: header.setDefaultAlignment(Qt.AlignCenter) else: header.setDefaultAlignment(Qt.AlignRight) self.cb_sel_list = [] # List of checkboxes def _setup_action_buttons(self): self.pb_remove_rel = QPushButton("Remove Rel.Int.(%) <") self.pb_remove_rel.clicked.connect(self.pb_remove_rel_clicked) self.le_remove_rel = LineEditExtended("") self._validator_le_remove_rel = QDoubleValidator() self._validator_le_remove_rel.setBottom(0.01) # Some small number self._validator_le_remove_rel.setTop(100.0) self.le_remove_rel.setText( self._format_threshold(self._remove_peak_threshold)) self._update_le_remove_rel_state() self.le_remove_rel.textChanged.connect(self.le_remove_rel_text_changed) self.le_remove_rel.editingFinished.connect( self.le_remove_rel_editing_finished) self.pb_remove_unchecked = QPushButton("Remove Unchecked Lines") self.pb_remove_unchecked.clicked.connect( self.pb_remove_unchecked_clicked) hbox = QHBoxLayout() hbox.addWidget(self.pb_remove_rel) hbox.addWidget(self.le_remove_rel) hbox.addStretch(1) hbox.addWidget(self.pb_remove_unchecked) return hbox def _set_tooltips(self): set_tooltip(self.cb_select_all, "<b>Select/Deselect All</b> emission lines in the list") set_tooltip(self.element_selection, "<b>Set active</b> emission line") set_tooltip(self.le_peak_intensity, "Set or modify <b>intensity</b> of the active peak.") set_tooltip(self.pb_add_eline, "<b>Add</b> emission line to the list.") set_tooltip(self.pb_remove_eline, "<b>Remove</b> emission line from the list.") set_tooltip( self.pb_user_peaks, "Open dialog box to add or modify parameters of the <b>user-defined peak</b>" ) set_tooltip( self.pb_pileup_peaks, "Open dialog box to add or modify parameters of the <b>pileup peak</b>" ) set_tooltip(self.tbl_elines, "The list of the selected <b>emission lines</b>") # set_tooltip(self.pb_update, # "Update the internally stored list of selected emission lines " # "and their parameters. This button is <b>deprecated</b>, but still may be " # "needed in some situations. In future releases it will be <b>removed</b> or replaced " # "with 'Accept' button. Substantial changes to the computational code is needed before " # "it happens.") # set_tooltip(self.pb_undo, # "<b>Undo</b> changes to the table of selected emission lines. Doesn't always work.") set_tooltip( self.pb_remove_rel, "<b>Remove emission lines</b> from the list if their relative intensity is less " "then specified threshold.", ) set_tooltip( self.le_remove_rel, "<b>Threshold</b> that controls which emission lines are removed " "when <b>Remove Rel.Int.(%)</b> button is pressed.", ) set_tooltip(self.pb_remove_unchecked, "Remove <b>unchecked</b> emission lines from the list.") def update_widget_state(self, condition=None): # Update the state of the menu bar state = not self.gui_vars["gui_state"]["running_computations"] self.setEnabled(state) # Hide the window if required by the program state state_file_loaded = self.gui_vars["gui_state"]["state_file_loaded"] state_model_exist = self.gui_vars["gui_state"]["state_model_exists"] if not state_file_loaded or not state_model_exist: self.hide() if condition == "tooltips": self._set_tooltips() def fill_eline_table(self, table_contents): self._table_contents = copy.deepcopy(table_contents) self._enable_events = False self.tbl_elines.clearContents() # Clear the list of checkboxes for cb in self.cb_sel_list: cb.stateChanged.connect(self.cb_eline_state_changed) self.cb_sel_list = [] self.tbl_elines.setRowCount(len(table_contents)) for nr, row in enumerate(table_contents): sel_status = row["sel_status"] row_data = [ None, row["z"], row["eline"], row["energy"], row["peak_int"], row["rel_int"], row["cs"] ] for nc, entry in enumerate(row_data): label = self.tbl_labels[nc] # Set alternating background colors for the table rows # Make background for editable items a little brighter brightness = 240 if label in self.tbl_cols_editable else 220 if nr % 2: rgb_bckg = (255, brightness, brightness) # Light-red else: rgb_bckg = (brightness, 255, brightness) # Light-green if nc == 0: item = QWidget() cb = CheckBoxNamed(name=f"{nr}") item_hbox = QHBoxLayout(item) item_hbox.addWidget(cb) item_hbox.setAlignment(Qt.AlignCenter) item_hbox.setContentsMargins(0, 0, 0, 0) css1 = get_background_css(rgb_bckg, widget="QCheckbox", editable=False) css2 = get_background_css(rgb_bckg, widget="QWidget", editable=False) item.setStyleSheet(css2 + css1) cb.setChecked(Qt.Checked if sel_status else Qt.Unchecked) cb.stateChanged.connect(self.cb_eline_state_changed) cb.setStyleSheet("QCheckBox {color: black;}") self.cb_sel_list.append(cb) self.tbl_elines.setCellWidget(nr, nc, item) else: s = None # The case when the value (Rel. Int.) is limited from the bottom # We don't want to print very small numbers here if label in self.tbl_value_min: v = self.tbl_value_min[label] if isinstance(entry, (float, np.float64)) and (entry < v): s = f"<{v:.2f}" if s is None: if isinstance(entry, (float, np.float64)): s = f"{entry:.2f}" if entry else "-" else: s = f"{entry}" if entry else "-" item = QTableWidgetItem(s) item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) # Set all columns not editable (unless needed) if label not in self.tbl_cols_editable: item.setFlags(item.flags() & ~Qt.ItemIsEditable) brush = QBrush(QColor(*rgb_bckg)) item.setBackground(brush) self.tbl_elines.setItem(nr, nc, item) self._enable_events = True # Update the rest of the widgets self._update_widgets_based_on_table_state() @Slot() def update_widget_data(self): # This is typically a new set of emission lines. Clear the selection both # in the table and in the element selection tool. self.element_selection.set_current_item("") self.tbl_elines.clearSelection() self._set_selected_eline("") # Now update the tables self._update_eline_selection_list() self.update_eline_table() self._update_add_remove_btn_state() def pb_pileup_peaks_clicked(self): data = {} eline = self._selected_eline if self.gpc.get_eline_name_category(eline) == "pileup": logger.error( f"Attempt to add pileup peak '{eline}' while another pileup peak is selected." ) return energy, marker_visible = self.gpc.get_suggested_manual_peak_energy() best_guess = self.gpc.get_guessed_pileup_peak_components(energy=energy, tolerance=0.1) if best_guess is not None: el1, el2, energy = best_guess else: # No peaks were found, enter peaks manually el1, el2, energy = "", "", 0 data["element1"] = el1 data["element2"] = el2 data["energy"] = energy data["range_low"], data[ "range_high"] = self.gpc.get_selected_energy_range() if not marker_visible: # We shouldn't end up here, but this will protect from crashing in case # the button was not disabled (a bug). msg = "Select location of the new peak center (energy)\nby clicking on the plot in 'Fit Model' tab" msgbox = QMessageBox(QMessageBox.Information, "User Input Required", msg, QMessageBox.Ok, parent=self) msgbox.exec() else: dlg = DialogPileupPeakParameters() def func(): def f(e1, e2): try: name = self.gpc.generate_pileup_peak_name(e1, e2) e = self.gpc.get_pileup_peak_energy(name) except Exception: e = 0 return e return f dlg.set_compute_energy_function(func()) dlg.set_parameters(data) if dlg.exec(): print("Pileup peak is added") try: data = dlg.get_parameters() eline1, eline2 = data["element1"], data["element2"] eline = self.gpc.generate_pileup_peak_name(eline1, eline2) self.gpc.add_peak_manual(eline) self.update_eline_table() # Update the table self.tbl_elines_set_selection( eline) # Select new emission line self._set_selected_eline(eline) self._set_fit_status(False) logger.info(f"New pileup peak {eline} was added") except RuntimeError as ex: msg = str(ex) msgbox = QMessageBox(QMessageBox.Critical, "Error", msg, QMessageBox.Ok, parent=self) msgbox.exec() # Reload the table anyway (nothing is going to be selected) self.update_eline_table() def pb_user_peaks_clicked(self): eline = self._selected_eline # If current peak is user_defined peak is_userpeak = self.gpc.get_eline_name_category(eline) == "userpeak" if is_userpeak: data = {} data["enabled"] = True data["name"] = eline data["maxv"] = self.gpc.get_eline_intensity(eline) data["energy"], data[ "fwhm"] = self.gpc.get_current_userpeak_energy_fwhm() dlg = DialogEditUserPeakParameters() dlg.set_parameters(data=data) if dlg.exec(): print("Editing of user defined peak is completed") try: eline = data["name"] data = dlg.get_parameters() self.gpc.update_userpeak(data["name"], data["energy"], data["maxv"], data["fwhm"]) self._set_fit_status(False) logger.info(f"User defined peak {eline} was updated.") except Exception as ex: msg = str(ex) msgbox = QMessageBox(QMessageBox.Critical, "Error", msg, QMessageBox.Ok, parent=self) msgbox.exec() # Reload the table anyway (nothing is going to be selected) self.update_eline_table() else: data = {} data["name"] = self.gpc.get_unused_userpeak_name() data[ "energy"], marker_visible = self.gpc.get_suggested_manual_peak_energy( ) if marker_visible: dlg = DialogNewUserPeak() dlg.set_parameters(data=data) if dlg.exec(): try: eline = data["name"] self.gpc.add_peak_manual(eline) self.update_eline_table() # Update the table self.tbl_elines_set_selection( eline) # Select new emission line self._set_selected_eline(eline) self._set_fit_status(False) logger.info(f"New user defined peak {eline} is added") except RuntimeError as ex: msg = str(ex) msgbox = QMessageBox(QMessageBox.Critical, "Error", msg, QMessageBox.Ok, parent=self) msgbox.exec() # Reload the table anyway (nothing is going to be selected) self.update_eline_table() else: msg = ("Select location of the new peak center (energy)\n" "by clicking on the plot in 'Fit Model' tab") msgbox = QMessageBox(QMessageBox.Information, "User Input Required", msg, QMessageBox.Ok, parent=self) msgbox.exec() @Slot() def pb_add_eline_clicked(self): logger.debug("'Add line' clicked") # It is assumed that this button is active only if an element is selected from the list # of available emission lines. It can't be used to add user-defined peaks or pileup peaks. eline = self._selected_eline if eline: try: self.gpc.add_peak_manual(eline) self.update_eline_table() # Update the table self.tbl_elines_set_selection( eline) # Select new emission line self._set_fit_status(False) except RuntimeError as ex: msg = str(ex) msgbox = QMessageBox(QMessageBox.Critical, "Error", msg, QMessageBox.Ok, parent=self) msgbox.exec() # Reload the table anyway (nothing is going to be selected) self.update_eline_table() @Slot() def pb_remove_eline_clicked(self): logger.debug("'Remove line' clicked") eline = self._selected_eline if eline: # If currently selected line is the emission line (like Ca_K), we want # it to remain selected after it is deleted. This means that nothing is selected # in the table. For other lines, nothing should remain selected. self.tbl_elines.clearSelection() self.gpc.remove_peak_manual(eline) self.update_eline_table() # Update the table if self.gpc.get_eline_name_category(eline) != "eline": eline = "" # This will update widgets self._set_selected_eline(eline) self._set_fit_status(False) def cb_select_all_toggled(self, state): self._enable_events = False eline_list, state_list = [], [] for n_row in range(self.tbl_elines.rowCount()): eline = self._table_contents[n_row]["eline"] # Do not deselect lines in category 'other'. They probably never to be deleted. # They also could be deselected manually. if self.gpc.get_eline_name_category( eline) == "other" and not state: to_check = True else: to_check = state self.cb_sel_list[n_row].setChecked( Qt.Checked if to_check else Qt.Unchecked) eline_list.append(eline) state_list.append(to_check) self.gpc.set_checked_emission_lines(eline_list, state_list) self._set_fit_status(False) self._enable_events = True def cb_eline_state_changed(self, name, state): if self._enable_events: n_row = int(name) state = state == Qt.Checked eline = self._table_contents[n_row]["eline"] self.gpc.set_checked_emission_lines([eline], [state]) self._set_fit_status(False) def tbl_elines_item_changed(self, item): if self._enable_events: n_row, n_col = self.tbl_elines.row(item), self.tbl_elines.column( item) # Value was changed if n_col == 4: text = item.text() eline = self._table_contents[n_row]["eline"] if self._validator_peak_height.validate( text, 0)[0] != QDoubleValidator.Acceptable: val = self._table_contents[n_row]["peak_int"] self._enable_events = False item.setText(f"{val:.2f}") self._enable_events = True self._set_fit_status(False) else: self.gpc.update_eline_peak_height(eline, float(text)) self.update_eline_table() def tbl_elines_item_selection_changed(self): sel_ranges = self.tbl_elines.selectedRanges() # The table is configured to have one or no selected ranges # 'Open' button should be enabled only if a range (row) is selected if sel_ranges: index = sel_ranges[0].topRow() eline = self._table_contents[index]["eline"] if self._enable_events: self._enable_events = False self._set_selected_eline(eline) self.element_selection.set_current_item(eline) self._enable_events = True def tbl_elines_set_selection(self, eline): """ Select the row with emission line `eline` in the table. Deselect everything if the emission line does not exist. """ index = self._get_eline_index_in_table(eline) self.tbl_elines.clearSelection() if index >= 0: self.tbl_elines.selectRow(index) def element_selection_item_changed(self, index, eline): self.signal_selected_element_changed.emit(eline) if self._enable_events: self._enable_events = False self._set_selected_eline(eline) self.tbl_elines_set_selection(eline) self._enable_events = True def pb_remove_rel_clicked(self): try: self.gpc.remove_peaks_below_threshold(self._remove_peak_threshold) except Exception as ex: msg = str(ex) msgbox = QMessageBox(QMessageBox.Critical, "Error", msg, QMessageBox.Ok, parent=self) msgbox.exec() self.update_eline_table() # Update the displayed estimated peak amplitude value 'le_peak_intensity' self._set_selected_eline(self._selected_eline) self._set_fit_status(False) def le_remove_rel_text_changed(self, text): self._update_le_remove_rel_state(text) def le_remove_rel_editing_finished(self): text = self.le_remove_rel.text() if self._validator_le_remove_rel.validate( text, 0)[0] == QDoubleValidator.Acceptable: self._remove_peak_threshold = float(text) else: self.le_remove_rel.setText( self._format_threshold(self._remove_peak_threshold)) def pb_remove_unchecked_clicked(self): try: self.gpc.remove_unchecked_peaks() except Exception as ex: msg = str(ex) msgbox = QMessageBox(QMessageBox.Critical, "Error", msg, QMessageBox.Ok, parent=self) msgbox.exec() # Reload the table self.update_eline_table() # Update the displayed estimated peak amplitude value 'le_peak_intensity' self._set_selected_eline(self._selected_eline) self._set_fit_status(False) def _display_peak_intensity(self, eline): v = self.gpc.get_eline_intensity(eline) s = f"{v:.10g}" if v is not None else "" self.le_peak_intensity.setText(s) def _update_le_remove_rel_state(self, text=None): if text is None: text = self.le_remove_rel.text() state = self._validator_le_remove_rel.validate( text, 0)[0] == QDoubleValidator.Acceptable self.le_remove_rel.setValid(state) self.pb_remove_rel.setEnabled(state) @Slot(str) def slot_selection_item_changed(self, eline): self.element_selection.set_current_item(eline) @Slot(bool) def slot_marker_state_changed(self, state): # If userpeak is selected and plot is clicked (marker is set), then user # should be allowed to add userpeak at a new location. So deselect the userpeak # from the table (if it is selected) logger.debug( f"Vertical marker on the fit plot changed state to {state}.") if state: self._deselect_special_peak_in_table() # Now update state of all buttons self._update_add_remove_btn_state() self._update_add_edit_userpeak_btn_state() self._update_add_edit_pileup_peak_btn_state() def _format_threshold(self, value): return f"{value:.2f}" def _deselect_special_peak_in_table(self): """Deselect userpeak if a userpeak is selected""" if self.gpc.get_eline_name_category( self._selected_eline) in ("userpeak", "pileup"): # Clear all selections self.tbl_elines_set_selection("") self._set_selected_eline("") # We also want to show marker at the new position self.gpc.show_marker_at_current_position() def _update_widgets_based_on_table_state(self): index, eline = self._get_current_index_in_table() if index >= 0: # Selection exists. Update the state of element selection widget. self.element_selection.set_current_item(eline) else: # No selection, update the state based on element selection widget. eline = self._selected_eline self.tbl_elines_set_selection(eline) self._update_add_remove_btn_state(eline) self._update_add_edit_userpeak_btn_state() self._update_add_edit_pileup_peak_btn_state() def _update_eline_selection_list(self): self._eline_list = self.gpc.get_full_eline_list() self.element_selection.set_item_list(self._eline_list) self.signal_update_element_selection_list.emit() @Slot() def update_eline_table(self): """Update table of emission lines without changing anything else""" eline_table = self.gpc.get_selected_eline_table() self.fill_eline_table(eline_table) def _get_eline_index_in_table(self, eline): try: index = [_["eline"] for _ in self._table_contents].index(eline) except ValueError: index = -1 return index def _get_eline_index_in_list(self, eline): try: index = self._eline_list.index(eline) except ValueError: index = -1 return index def _get_current_index_in_table(self): sel_ranges = self.tbl_elines.selectedRanges() # The table is configured to have one or no selected ranges # 'Open' button should be enabled only if a range (row) is selected if sel_ranges: index = sel_ranges[0].topRow() eline = self._table_contents[index]["eline"] else: index, eline = -1, "" return index, eline def _get_current_index_in_list(self): index, eline = self.element_selection.get_current_item() return index, eline def _update_add_remove_btn_state(self, eline=None): if eline is None: index_in_table, eline = self._get_current_index_in_table() index_in_list, eline = self._get_current_index_in_list() else: index_in_table = self._get_eline_index_in_table(eline) index_in_list = self._get_eline_index_in_list(eline) add_enabled, remove_enabled = True, True if index_in_list < 0 and index_in_table < 0: add_enabled, remove_enabled = False, False else: if index_in_table >= 0: if self.gpc.get_eline_name_category(eline) != "other": add_enabled = False else: add_enabled, remove_enabled = False, False else: remove_enabled = False self.pb_add_eline.setEnabled(add_enabled) self.pb_remove_eline.setEnabled(remove_enabled) self.signal_update_add_remove_btn_state.emit(add_enabled, remove_enabled) def _update_add_edit_userpeak_btn_state(self): enabled = True add_peak = True if self.gpc.get_eline_name_category( self._selected_eline) == "userpeak": add_peak = False # Finally check if marker is set (you need it for adding peaks) _, marker_set = self.gpc.get_suggested_manual_peak_energy() if not marker_set and add_peak: enabled = False if add_peak: btn_text = "New User Peak ..." else: btn_text = "Edit User Peak ..." self.pb_user_peaks.setText(btn_text) self.pb_user_peaks.setEnabled(enabled) def _update_add_edit_pileup_peak_btn_state(self): enabled = True if self.gpc.get_eline_name_category(self._selected_eline) == "pileup": enabled = False # Finally check if marker is set (you need it for adding peaks) _, marker_set = self.gpc.get_suggested_manual_peak_energy() # Ignore set marker for userpeaks (marker is used to display location of userpeaks) if self.gpc.get_eline_name_category( self._selected_eline) == "userpeak": marker_set = False if not marker_set: enabled = False self.pb_pileup_peaks.setEnabled(enabled) def _set_selected_eline(self, eline): self._update_add_remove_btn_state(eline) if eline != self._selected_eline: self._selected_eline = eline self.gpc.set_selected_eline(eline) self._display_peak_intensity(eline) else: # Peak intensity may change in some circumstances, so renew the displayed value. self._display_peak_intensity(eline) # Update button states after 'self._selected_eline' is set self._update_add_edit_userpeak_btn_state() self._update_add_edit_pileup_peak_btn_state() def _set_fit_status(self, status): self.gui_vars["gui_state"]["state_model_fit_exists"] = status self.signal_parameters_changed.emit()
class WndDetailedFittingParams(SecondaryWindow): # Signal that is sent (to main window) to update global state of the program update_global_state = Signal() computations_complete = Signal(object) def __init__(self, *, window_title, gpc, gui_vars): super().__init__() # Global processing classes self.gpc = gpc # Global GUI variables (used for control of GUI state) self.gui_vars = gui_vars # Reference to the main window. The main window will hold # references to all non-modal windows that could be opened # from multiple places in the program. self.ref_main_window = self.gui_vars["ref_main_window"] self.update_global_state.connect( self.ref_main_window.update_widget_state) self._enable_events = False self._dialog_data = {} self._load_dialog_data() self._selected_index = 0 self._selected_eline = "-" self.setWindowTitle(window_title) self.setMinimumWidth(1100) self.setMinimumHeight(500) self.resize(1100, 500) hbox_el_select = self._setup_element_selection() self._setup_table() vbox = QVBoxLayout() vbox.addLayout(hbox_el_select) vbox.addWidget(self.table) self.setLayout(vbox) self.update_form_data() self._enable_events = True self._data_changed = False def _setup_element_selection(self): self.combo_element_sel = QComboBox() self.combo_element_sel.setMinimumWidth(200) self.combo_element_sel.currentIndexChanged.connect( self.combo_element_sel_current_index_changed) self.pb_apply = QPushButton("Apply") self.pb_apply.setEnabled(False) self.pb_apply.clicked.connect(self.pb_apply_clicked) self.pb_cancel = QPushButton("Cancel") self.pb_cancel.setEnabled(False) self.pb_cancel.clicked.connect(self.pb_cancel_clicked) hbox = QHBoxLayout() hbox.addWidget(QLabel("Select element:")) hbox.addWidget(self.combo_element_sel) hbox.addStretch(1) hbox.addWidget(self.pb_apply) hbox.addWidget(self.pb_cancel) return hbox def _setup_table(self): self._value_keys = ("value", "min", "max") # Labels for horizontal header labels_presets = [ fitting_preset_names[_] for _ in self._fit_strategy_list ] labels_values = [value_names[_] for _ in self._value_keys] self.tbl_labels = ["Name", "E, keV"] + labels_values + labels_presets # Labels for editable columns self.tbl_cols_editable = ("Value", "Min", "Max") # Labels for the columns that contain combo boxes self.tbl_cols_combobox = labels_presets # The list of columns with fixed size self.tbl_cols_stretch = ("Value", "Min", "Max") # Table item representation if different from default self.tbl_format = { "E, keV": ".4f", "Value": ".8g", "Min": ".8g", "Max": ".8g" } # Combobox items. All comboboxes in the table contain identical list of items. self.combo_items = self._bound_options self._combo_list = [] self.table = QTableWidget() self.table.setColumnCount(len(self.tbl_labels)) self.table.verticalHeader().hide() self.table.setHorizontalHeaderLabels(self.tbl_labels) self.table.setStyleSheet("QTableWidget::item{color: black;}") header = self.table.horizontalHeader() for n, lbl in enumerate(self.tbl_labels): # Set stretching for the columns if lbl in self.tbl_cols_stretch: header.setSectionResizeMode(n, QHeaderView.Stretch) else: header.setSectionResizeMode(n, QHeaderView.ResizeToContents) self.table.itemChanged.connect(self.tbl_elines_item_changed) def _fill_table(self, table_contents): self._enable_events = False # Clear the list of combo boxes for item in self._combo_list: item.currentIndexChanged.disconnect( self.combo_strategy_current_index_changed) self._combo_list = [] self.table.clearContents() self.table.setRowCount(len(table_contents)) for nr, row in enumerate(table_contents): n_fit_strategy = 0 row_name = row[0] for nc, entry in enumerate(row): label = self.tbl_labels[nc] # Set alternating background colors for the table rows # Make background for editable items a little brighter brightness = 240 if label in self.tbl_cols_editable else 220 if nr % 2: rgb_bckg = (255, brightness, brightness) else: rgb_bckg = (brightness, 255, brightness) if label not in self.tbl_cols_combobox: if label in self.tbl_format and not isinstance(entry, str): fmt = self.tbl_format[self.tbl_labels[nc]] s = ("{:" + fmt + "}").format(entry) else: s = f"{entry}" item = QTableWidgetItem(s) if nc > 0: item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) # Set all columns not editable (unless needed) if label not in self.tbl_cols_editable: item.setFlags(item.flags() & ~Qt.ItemIsEditable) # Make all items not selectable (we are not using selections) item.setFlags(item.flags() & ~Qt.ItemIsSelectable) # Note, that there is no way to set style sheet for QTableWidgetItem item.setBackground(QBrush(QColor(*rgb_bckg))) self.table.setItem(nr, nc, item) else: if n_fit_strategy < len(self._fit_strategy_list): combo_name = f"{row_name},{self._fit_strategy_list[n_fit_strategy]}" else: combo_name = "" n_fit_strategy += 1 item = ComboBoxNamedNoWheel(name=combo_name) # Set text color for QComboBox widget (necessary if the program is used with Dark theme) pal = item.palette() pal.setColor(QPalette.ButtonText, Qt.black) item.setPalette(pal) # Set text color for drop-down view (necessary if the program is used with Dark theme) pal = item.view().palette() pal.setColor(QPalette.Text, Qt.black) item.view().setPalette(pal) css1 = get_background_css(rgb_bckg, widget="QComboBox", editable=False) css2 = get_background_css(rgb_bckg, widget="QWidget", editable=False) item.setStyleSheet(css2 + css1) item.addItems(self.combo_items) if item.findText(entry) < 0: logger.warning( f"Text '{entry}' is not found. The ComboBox is not set properly." ) item.setCurrentText(entry) # Try selecting the item anyway self.table.setCellWidget(nr, nc, item) item.currentIndexChanged.connect( self.combo_strategy_current_index_changed) self._combo_list.append(item) self._enable_events = True def _set_tooltips(self): set_tooltip(self.pb_apply, "Save changes and <b>update plots</b>.") set_tooltip(self.pb_cancel, "<b>Discard</b> all changes.") set_tooltip( self.combo_element_sel, "Select K, L or M <b>emission line</b> to edit the optimization parameters " "used for the line during total spectrum fitting.", ) set_tooltip( self.table, "Edit optimization parameters for the selected emission line. " "Processing presets may be configured by specifying optimization strategy " "for each parameter may be selected. A preset for each fitting step " "of the total spectrum fitting may be selected in <b>Model</b> tab.", ) def combo_element_sel_current_index_changed(self, index): self._selected_index = index try: self._selected_eline = self._eline_list[index] except Exception: self._selected_eline = "-" self._update_table() def combo_strategy_current_index_changed(self, name, index): if self._enable_events: try: name_row, name_strategy = name.split(",") option = self._bound_options[index] self._param_dict[name_row][name_strategy] = option except Exception as ex: logger.error( f"Error occurred while changing strategy options: {ex}") self._data_changed = True self._validate_all() def tbl_elines_item_changed(self, item): if self._enable_events: try: n_row, n_col = self.table.row(item), self.table.column(item) n_key = n_col - 2 if n_key < 0 or n_key >= len(self._value_keys): raise RuntimeError(f"Incorrect column {n_col}") value_key = self._value_keys[n_key] eline_key = self._table_contents[n_row][0] try: value = float(item.text()) self._param_dict[eline_key][value_key] = value except Exception: value = self._param_dict[eline_key][value_key] item.setText(f"{value:.8g}") self._data_changed = True self._validate_all() except Exception as ex: logger.error( f"Error occurred while setting edited value: {ex}") def pb_apply_clicked(self): """Save dialog data and update plots""" self.save_form_data() def pb_cancel_clicked(self): """Reload data (discard all changes)""" self.update_form_data() def _set_combo_element_sel_items(self): element_list = self._eline_list self.combo_element_sel.clear() self.combo_element_sel.addItems(element_list) # Deselect all (this should clear the table) self.select_eline(self._selected_eline) def _set_dialog_data(self, dialog_data): self._param_dict = dialog_data["param_dict"] self._eline_list = dialog_data["eline_list"] self._eline_key_dict = dialog_data["eline_key_dict"] self._eline_energy_dict = dialog_data["eline_energy_dict"] self._other_param_list = dialog_data["other_param_list"] self._fit_strategy_list = dialog_data["fit_strategy_list"] self._bound_options = dialog_data["bound_options"] def _show_all(self): selected_eline = self._selected_eline self._set_combo_element_sel_items() self.select_eline(selected_eline) self._update_table() def _update_table(self): self._enable_events = False eline_list = [] if "shared" in self._selected_eline.lower(): # Shared parameters eline_list = self._other_param_list energy_list = [""] * len(eline_list) elif self._selected_eline == self._eline_list[ self._selected_index]: # Emission lines eline = self._selected_eline eline_list = self._eline_key_dict[eline] energy_list = self._eline_energy_dict[eline] self._table_contents = [] for n, key in enumerate(eline_list): data = [ key, energy_list[n], self._param_dict[key]["value"], self._param_dict[key]["min"], self._param_dict[key]["max"], ] for strategy in self._fit_strategy_list: data.append(self._param_dict[key][strategy]) self._table_contents.append(data) self._fill_table(self._table_contents) self._enable_events = True def select_eline(self, eline): if eline in self._eline_list: index = self._eline_list.index(eline) elif self._eline_list: index = 0 eline = self._eline_list[0] else: index = -1 eline = "-" self._selected_eline = eline self._selected_index = index self.combo_element_sel.setCurrentIndex(index) self._update_table() def update_widget_state(self, condition=None): # Update the state of the menu bar state = not self.gui_vars["gui_state"]["running_computations"] self.setEnabled(state) if condition == "tooltips": self._set_tooltips() def _validate_all(self): self.pb_apply.setEnabled(self._data_changed) self.pb_cancel.setEnabled(self._data_changed) def _load_dialog_data(self): ... def _save_dialog_data_function(self): ... def update_form_data(self): self._load_dialog_data() self._show_all() self._data_changed = False self._validate_all() def save_form_data(self): if self._data_changed: f_save_data = self._save_dialog_data_function() def cb(dialog_data): try: f_save_data(dialog_data) success, msg = True, "" except Exception as ex: success, msg = False, str(ex) return {"success": success, "msg": msg} self._compute_in_background(cb, self.slot_save_form_data, dialog_data=self._dialog_data) @Slot(object) def slot_save_form_data(self, result): self._recover_after_compute(self.slot_save_form_data) if not result["success"]: msg = result["msg"] msgbox = QMessageBox(QMessageBox.Critical, "Failed to Apply Fit Parameters", msg, QMessageBox.Ok, parent=self) msgbox.exec() else: self._data_changed = False self._validate_all() self.gui_vars["gui_state"]["state_model_fit_exists"] = False self.update_global_state.emit() def _compute_in_background(self, func, slot, *args, **kwargs): """ Run function `func` in a background thread. Send the signal `self.computations_complete` once computation is finished. Parameters ---------- func: function Reference to a function that is supposed to be executed at the background. The function return value is passed as a signal parameter once computation is complete. slot: qtpy.QtCore.Slot or None Reference to a slot. If not None, then the signal `self.computation_complete` is connected to this slot. args, kwargs arguments of the function `func`. """ signal_complete = self.computations_complete def func_to_run(func, *args, **kwargs): class RunTask(QRunnable): def run(self): result_dict = func(*args, **kwargs) signal_complete.emit(result_dict) return RunTask() if slot is not None: self.computations_complete.connect(slot) self.gui_vars["gui_state"]["running_computations"] = True self.update_global_state.emit() QThreadPool.globalInstance().start(func_to_run(func, *args, **kwargs)) def _recover_after_compute(self, slot): """ The function should be called after the signal `self.computations_complete` is received. The slot should be the same as the one used when calling `self.compute_in_background`. """ if slot is not None: self.computations_complete.disconnect(slot) self.gui_vars["gui_state"]["running_computations"] = False self.update_global_state.emit()
class ShortcutEditor(QWidget): """Widget to edit keybindings for napari.""" valueChanged = Signal(dict) VIEWER_KEYBINDINGS = trans._('Viewer key bindings') def __init__( self, parent: QWidget = None, description: str = "", value: dict = None, ): super().__init__(parent=parent) # Flag to not run _set_keybinding method after setting special symbols. # When changing line edit to special symbols, the _set_keybinding # method will be called again (and breaks) and is not needed. self._skip = False layers = [ Image, Labels, Points, Shapes, Surface, Vectors, ] self.key_bindings_strs = OrderedDict() # widgets self.layer_combo_box = QComboBox(self) self._label = QLabel(self) self._table = QTableWidget(self) self._table.setSelectionBehavior(QAbstractItemView.SelectItems) self._table.setSelectionMode(QAbstractItemView.SingleSelection) self._table.setShowGrid(False) self._restore_button = QPushButton(trans._("Reset All Keybindings")) # Set up dictionary for layers and associated actions. all_actions = action_manager._actions.copy() self.key_bindings_strs[self.VIEWER_KEYBINDINGS] = [] for layer in layers: if len(layer.class_keymap) == 0: actions = [] else: actions = action_manager._get_layer_actions(layer) for name, action in actions.items(): all_actions.pop(name) self.key_bindings_strs[f"{layer.__name__} layer"] = actions # Left over actions can go here. self.key_bindings_strs[self.VIEWER_KEYBINDINGS] = all_actions # Widget set up self.layer_combo_box.addItems(list(self.key_bindings_strs)) self.layer_combo_box.activated[str].connect(self._set_table) self.layer_combo_box.setCurrentText(self.VIEWER_KEYBINDINGS) self._set_table() self._label.setText("Group") self._restore_button.clicked.connect(self.restore_defaults) # layout hlayout1 = QHBoxLayout() hlayout1.addWidget(self._label) hlayout1.addWidget(self.layer_combo_box) hlayout1.setContentsMargins(0, 0, 0, 0) hlayout1.setSpacing(20) hlayout1.addStretch(0) hlayout2 = QHBoxLayout() hlayout2.addLayout(hlayout1) hlayout2.addWidget(self._restore_button) layout = QVBoxLayout() layout.addLayout(hlayout2) layout.addWidget(self._table) self.setLayout(layout) def restore_defaults(self): """Launches dialog to confirm restore choice.""" self._reset_dialog = ConfirmDialog( parent=self, text=trans._( "Are you sure you want to restore default shortcuts?" ), ) self._reset_dialog.valueChanged.connect(self._reset_shortcuts) self._reset_dialog.exec_() def _reset_shortcuts(self, event=None): """Reset shortcuts to default settings. Parameters ---------- event: Bool Event will indicate whether user confirmed resetting shortcuts. """ # event is True if the user confirmed reset shortcuts if event is True: get_settings().reset(sections=['shortcuts']) for ( action, shortcuts, ) in get_settings().shortcuts.shortcuts.items(): action_manager.unbind_shortcut(action) for shortcut in shortcuts: action_manager.bind_shortcut(action, shortcut) self._set_table(layer_str=self.layer_combo_box.currentText()) def _set_table(self, layer_str=''): """Builds and populates keybindings table. Parameters ---------- layer_str = str If layer_str is not empty, then show the specified layers' keybinding shortcut table. """ # Keep track of what is in each column. self._action_name_col = 0 self._icon_col = 1 self._shortcut_col = 2 self._action_col = 3 # Set header strings for table. header_strs = ['', '', '', ''] header_strs[self._action_name_col] = trans._('Action') header_strs[self._shortcut_col] = trans._('Keybinding') # If no layer_str, then set the page to the viewer keybindings page. if layer_str == '': layer_str = self.VIEWER_KEYBINDINGS # If rebuilding the table, then need to disconnect the connection made # previously as well as clear the table contents. try: self._table.cellChanged.disconnect(self._set_keybinding) except TypeError: # if building the first time, the cells are not yet connected so this would fail. pass except RuntimeError: # Needed to pass some tests. pass self._table.clearContents() # Table styling set up. self._table.horizontalHeader().setStretchLastSection(True) self._table.horizontalHeader().setStyleSheet( 'border-bottom: 2px solid white;' ) # Get all actions for the layer. actions = self.key_bindings_strs[layer_str] if len(actions) > 0: # Set up table based on number of actions and needed columns. self._table.setRowCount(len(actions)) self._table.setColumnCount(4) # Set up delegate in order to capture keybindings. self._table.setItemDelegateForColumn( self._shortcut_col, ShortcutDelegate(self._table) ) self._table.setHorizontalHeaderLabels(header_strs) self._table.verticalHeader().setVisible(False) # Hide the column with action names. These are kept here for reference when needed. self._table.setColumnHidden(self._action_col, True) # Column set up. self._table.setColumnWidth(self._action_name_col, 250) self._table.setColumnWidth(self._shortcut_col, 200) self._table.setColumnWidth(self._icon_col, 50) # Go through all the actions in the layer and add them to the table. for row, (action_name, action) in enumerate(actions.items()): shortcuts = action_manager._shortcuts.get(action_name, []) # Set action description. Make sure its not selectable/editable. item = QTableWidgetItem(action.description) item.setFlags(Qt.NoItemFlags) self._table.setItem(row, self._action_name_col, item) # Create empty item in order to make sure this column is not # selectable/editable. item = QTableWidgetItem("") item.setFlags(Qt.NoItemFlags) self._table.setItem(row, self._icon_col, item) # Set the shortcuts in table. item_shortcut = QTableWidgetItem( Shortcut(list(shortcuts)[0]).platform if shortcuts else "" ) self._table.setItem(row, self._shortcut_col, item_shortcut) # action_name is stored in the table to use later, but is not shown on dialog. item_action = QTableWidgetItem(action_name) self._table.setItem(row, self._action_col, item_action) # If a cell is changed, run .set_keybinding. self._table.cellChanged.connect(self._set_keybinding) else: # Display that there are no actions for this layer. self._table.setRowCount(1) self._table.setColumnCount(4) self._table.setHorizontalHeaderLabels(header_strs) self._table.verticalHeader().setVisible(False) self._table.setColumnHidden(self._action_col, True) item = QTableWidgetItem('No key bindings') item.setFlags(Qt.NoItemFlags) self._table.setItem(0, 0, item) def _set_keybinding(self, row, col): """Checks the new keybinding to determine if it can be set. Parameters ---------- row: int Row in keybindings table that is being edited. col: int Column being edited (shortcut column). """ if self._skip is True: # Do nothing if the text is setting to a symbol. # Its already been handled. self._skip = False return if col == self._shortcut_col: # Get all layer actions and viewer actions in order to determine # the new shortcut is not already set to an action. current_layer_text = self.layer_combo_box.currentText() layer_actions = self.key_bindings_strs[current_layer_text] actions_all = layer_actions.copy() if current_layer_text is not self.VIEWER_KEYBINDINGS: viewer_actions = self.key_bindings_strs[ self.VIEWER_KEYBINDINGS ] actions_all.update(viewer_actions) # get the current item from shortcuts column current_item = self._table.currentItem() new_shortcut = current_item.text() new_shortcut = new_shortcut[0].upper() + new_shortcut[1:] # get the current action name current_action = self._table.item(row, self._action_col).text() # get the original shortcutS current_shortcuts = list( action_manager._shortcuts.get(current_action, {}) ) # Flag to indicate whether to set the new shortcut. replace = True # Go through all layer actions to determine if the new shortcut is already here. for row1, (action_name, action) in enumerate(actions_all.items()): shortcuts = action_manager._shortcuts.get(action_name, []) if new_shortcut in shortcuts: # Shortcut is here (either same action or not), don't replace in settings. replace = False if action_name != current_action: # the shortcut is saved to a different action # show warning symbols self._show_warning_icons([row, row1]) # show warning message message = trans._( "The keybinding <b>{new_shortcut}</b> " + "is already assigned to <b>{action_description}</b>; change or clear " + "that shortcut before assigning <b>{new_shortcut}</b> to this one.", new_shortcut=new_shortcut, action_description=action.description, ) self._show_warning(new_shortcut, action, row, message) if len(current_shortcuts) > 0: # If there was a shortcut set originally, then format it and reset the text. format_shortcut = Shortcut( current_shortcuts[0] ).platform if format_shortcut != current_shortcuts[0]: # only skip the next round if there are special symbols self._skip = True current_item.setText(format_shortcut) else: # There wasn't a shortcut here. current_item.setText("") self._cleanup_warning_icons([row, row1]) break else: # This shortcut was here. Reformat and reset text. format_shortcut = Shortcut(new_shortcut).platform if format_shortcut != new_shortcut: # Only skip the next round if there are special symbols in shortcut. self._skip = True current_item.setText(format_shortcut) if replace is True: # This shortcut is not taken. # Unbind current action from shortcuts in action manager. action_manager.unbind_shortcut(current_action) if new_shortcut != "": # Bind the new shortcut. try: action_manager.bind_shortcut( current_action, new_shortcut ) except TypeError: # Shortcut is not valid. action_manager._shortcuts[current_action] = set() # need to rebind the old shortcut action_manager.unbind_shortcut(current_action) action_manager.bind_shortcut( current_action, current_shortcuts[0] ) # Show warning message to let user know this shortcut is invalid. self._show_warning_icons([row]) message = trans._( "<b>{new_shortcut}</b> is not a valid keybinding.", new_shortcut=new_shortcut, ) self._show_warning(new_shortcut, action, row, message) self._cleanup_warning_icons([row]) format_shortcut = Shortcut( current_shortcuts[0] ).platform if format_shortcut != current_shortcuts[0]: # Skip the next round if there are special symbols. self._skip = True # Update text to formated shortcut. current_item.setText(format_shortcut) return # The new shortcut is valid and can be displayed in widget. # Keep track of what changed. new_value_dict = {current_action: [new_shortcut]} # Format new shortcut. format_shortcut = Shortcut(new_shortcut).platform if format_shortcut != new_shortcut: # Skip the next round because there are special symbols. self._skip = True # Update text to formated shortcut. current_item.setText(format_shortcut) else: # There is not a new shortcut to bind. Keep track of it. if action_manager._shortcuts[current_action] != "": new_value_dict = {current_action: [""]} if new_value_dict: # Emit signal when new value set for shortcut. self.valueChanged.emit(new_value_dict) def _show_warning_icons(self, rows): """Creates and displays the warning icons. Parameters ---------- rows: list[int] List of row numbers that should have the icon. """ for row in rows: self.warning_indicator = QLabel(self) self.warning_indicator.setObjectName("error_label") self._table.setCellWidget( row, self._icon_col, self.warning_indicator ) def _cleanup_warning_icons(self, rows): """Remove the warning icons from the shortcut table. Paramters --------- rows: list[int] List of row numbers to remove warning icon from. """ for row in rows: self._table.setCellWidget(row, self._icon_col, QLabel("")) def _show_warning(self, new_shortcut='', action=None, row=0, message=''): """Creates and displays warning message when shortcut is already assigned. Parameters ---------- new_shortcut: str The new shortcut attempting to be set. action: Action Action that is already assigned with the shortcut. row: int Row in table where the shortcut is attempting to be set. message: str Message to be displayed in warning pop up. """ # Determine placement of warning message. delta_y = 105 delta_x = 10 global_point = self.mapToGlobal( QPoint( self._table.columnViewportPosition(self._shortcut_col) + delta_x, self._table.rowViewportPosition(row) + delta_y, ) ) # Create warning pop up and move it to desired position. self._warn_dialog = KeyBindWarnPopup( text=message, ) self._warn_dialog.move(global_point) # Styling adjustments. self._warn_dialog.resize(250, self._warn_dialog.sizeHint().height()) self._warn_dialog._message.resize( 200, self._warn_dialog._message.sizeHint().height() ) self._warn_dialog.exec_() def value(self): """Return the actions and shortcuts currently assigned in action manager. Returns ------- value: dict Dictionary of action names and shortcuts assigned to them. """ value = {} for row, (action_name, action) in enumerate( action_manager._actions.items() ): shortcuts = action_manager._shortcuts.get(action_name, []) value[action_name] = list(shortcuts) return value
class LevelsPresetDialog(QDialog): # name of the current preset; whether to set this preset as default; dict of Levels levels_changed = Signal(str, bool, dict) def __init__(self, parent, preset_name, levels): super().__init__(parent) self.preset_name = preset_name self.levels = deepcopy(levels) self.setupUi() self.update_output() def setupUi(self): self.resize(480, 340) self.vbox = QVBoxLayout(self) self.presetLabel = QLabel(self) self.table = QTableWidget(0, 4, self) self.setAsDefaultCheckbox = QCheckBox("Set as default preset", self) self.vbox.addWidget(self.presetLabel) self.vbox.addWidget(self.table) self.vbox.addWidget(self.setAsDefaultCheckbox) self.table.setEditTriggers(QTableWidget.NoEditTriggers) self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setHorizontalHeaderLabels( ["Show", "Level name", "Preview", "Preview (dark)"]) self.table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.table.horizontalHeader().setSectionsClickable(False) self.table.horizontalHeader().setSectionsMovable(False) self.table.horizontalHeader().setSectionResizeMode( 0, QHeaderView.ResizeToContents) self.table.verticalHeader().setVisible(False) self.table.doubleClicked.connect(self.open_level_edit_dialog) self.table.setContextMenuPolicy(Qt.CustomContextMenu) self.table.customContextMenuRequested.connect(self.open_menu) buttons = QDialogButtonBox.Reset | QDialogButtonBox.Save | QDialogButtonBox.Cancel self.buttonBox = QDialogButtonBox(buttons, self) self.vbox.addWidget(self.buttonBox) self.buttonBox.accepted.connect(self.accept) self.buttonBox.rejected.connect(self.reject) self.resetButton = self.buttonBox.button(QDialogButtonBox.Reset) self.resetButton.clicked.connect(self.reset) def update_output(self): self.presetLabel.setText("Preset: {}".format(self.preset_name)) self.setAsDefaultCheckbox.setChecked( CONFIG['default_levels_preset'] == self.preset_name) self.table.clearContents() self.table.setRowCount(len(self.levels)) for i, levelname in enumerate(self.levels): level = self.levels[levelname] checkbox = self.get_level_show_checkbox(level) nameItem = QTableWidgetItem(level.levelname) preview, previewDark = self.get_preview_items(level) self.table.setCellWidget(i, 0, checkbox) self.table.setItem(i, 1, nameItem) self.table.setItem(i, 2, preview) self.table.setItem(i, 3, previewDark) def get_level_show_checkbox(self, level): checkbox_widget = QWidget(self.table) checkbox_widget.setStyleSheet("QWidget { background-color:none;}") checkbox = QCheckBox() checkbox.setStyleSheet( "QCheckBox::indicator { width: 15px; height: 15px;}") checkbox.setChecked(level.enabled) checkbox_layout = QHBoxLayout() checkbox_layout.setAlignment(Qt.AlignCenter) checkbox_layout.setContentsMargins(0, 0, 0, 0) checkbox_layout.addWidget(checkbox) checkbox_widget.setLayout(checkbox_layout) return checkbox_widget def get_preview_items(self, level): previewItem = QTableWidgetItem("Log message") previewItem.setBackground(QBrush(level.bg, Qt.SolidPattern)) previewItem.setForeground(QBrush(level.fg, Qt.SolidPattern)) previewItemDark = QTableWidgetItem("Log message") previewItemDark.setBackground(QBrush(level.bgDark, Qt.SolidPattern)) previewItemDark.setForeground(QBrush(level.fgDark, Qt.SolidPattern)) font = QFont(CONFIG.logger_table_font, CONFIG.logger_table_font_size) fontDark = QFont(font) if 'bold' in level.styles: font.setBold(True) if 'italic' in level.styles: font.setItalic(True) if 'underline' in level.styles: font.setUnderline(True) if 'bold' in level.stylesDark: fontDark.setBold(True) if 'italic' in level.stylesDark: fontDark.setItalic(True) if 'underline' in level.stylesDark: fontDark.setUnderline(True) previewItem.setFont(font) previewItemDark.setFont(fontDark) return previewItem, previewItemDark def open_level_edit_dialog(self, index): levelname = self.table.item(index.row(), 1).data(Qt.DisplayRole) level = self.levels[levelname] d = LevelEditDialog(self, level) d.setWindowModality(Qt.NonModal) d.setWindowTitle('Level editor') d.level_changed.connect(self.update_output) d.open() def open_menu(self, position): menu = QMenu(self) preset_menu = menu.addMenu('Presets') preset_menu.addAction('New preset', self.new_preset_dialog) preset_menu.addSeparator() preset_names = CONFIG.get_levels_presets() if len(preset_names) == 0: action = preset_menu.addAction('No presets') action.setEnabled(False) else: delete_menu = menu.addMenu('Delete preset') for name in preset_names: preset_menu.addAction(name, partial(self.load_preset, name)) delete_menu.addAction(name, partial(self.delete_preset, name)) menu.addSeparator() menu.addAction('New level...', self.create_new_level_dialog) if len(self.table.selectedIndexes()) > 0: menu.addAction('Delete selected', self.delete_selected) menu.popup(self.table.viewport().mapToGlobal(position)) def load_preset(self, name): new_levels = CONFIG.load_levels_preset(name) if not new_levels: return self.levels = new_levels self.preset_name = name self.update_output() def delete_preset(self, name): CONFIG.delete_levels_preset(name) if name == self.preset_name: self.reset() def delete_selected(self): selected = self.table.selectionModel().selectedRows() for index in selected: item = self.table.item(index.row(), 1) del self.levels[item.text()] self.update_output() def new_preset_dialog(self): d = QInputDialog(self) d.setLabelText('Enter the new name for the new preset:') d.setWindowTitle('Create new preset') d.textValueSelected.connect(self.create_new_preset) d.open() def create_new_preset(self, name): if name in CONFIG.get_levels_presets(): show_warning_dialog( self, "Preset creation error", 'Preset named "{}" already exists.'.format(name)) return if len(name.strip()) == 0: show_warning_dialog( self, "Preset creation error", 'This preset name is not allowed.'.format(name)) return self.preset_name = name self.update_output() CONFIG.save_levels_preset(name, self.levels) def create_new_level_dialog(self): d = LevelEditDialog(self, creating_new_level=True, level_names=self.levels.keys()) d.setWindowModality(Qt.NonModal) d.setWindowTitle('Level editor') d.level_changed.connect(self.level_changed) d.open() def level_changed(self, level): if level.levelname in self.levels: self.levels.copy_from(level) else: self.levels[level.levelname] = level self.update_output() def accept(self): for i, _ in enumerate(self.levels): checkbox = self.table.cellWidget(i, 0).children()[1] levelname = self.table.item(i, 1).text() self.levels[levelname].enabled = checkbox.isChecked() self.levels_changed.emit(self.preset_name, self.setAsDefaultCheckbox.isChecked(), self.levels) self.done(0) def reject(self): self.done(0) def reset(self): for levelname, level in self.levels.items(): level.copy_from(get_default_level(levelname)) self.update_output()
class D0(QGroupBox): def __init__(self, parent=None): self._parent = parent super().__init__(parent) self.setTitle("Define d₀") layout = QVBoxLayout() self.d0_grid_switch = QComboBox() self.d0_grid_switch.addItems(["Constant", "Field"]) self.d0_grid_switch.currentTextChanged.connect(self.set_case) layout.addWidget(self.d0_grid_switch) self.d0_box = QWidget() d0_box_layout = QHBoxLayout() d0_box_layout.addWidget(QLabel("d₀")) validator = QDoubleValidator() validator.setBottom(0) self.d0 = QLineEdit() self.d0.setValidator(validator) self.d0.editingFinished.connect(self.update_d0) d0_box_layout.addWidget(self.d0) d0_box_layout.addWidget(QLabel("Δd₀")) self.d0e = QLineEdit() self.d0e.setValidator(validator) self.d0e.editingFinished.connect(self.update_d0) d0_box_layout.addWidget(self.d0e) self.d0_box.setLayout(d0_box_layout) layout.addWidget(self.d0_box) load_save = QWidget() load_save_layout = QHBoxLayout() self.load_grid = QPushButton("Load d₀ Grid") self.load_grid.clicked.connect(self.load_d0_field) load_save_layout.addWidget(self.load_grid) self.save_grid = QPushButton("Save d₀ Grid") self.save_grid.clicked.connect(self.save_d0_field) load_save_layout.addWidget(self.save_grid) load_save.setLayout(load_save_layout) layout.addWidget(load_save) self.d0_grid = QTableWidget() self.d0_grid.setColumnCount(5) self.d0_grid.setColumnWidth(0, 60) self.d0_grid.setColumnWidth(1, 60) self.d0_grid.setColumnWidth(2, 60) self.d0_grid.setColumnWidth(3, 60) self.d0_grid.verticalHeader().setVisible(False) self.d0_grid.horizontalHeader().setStretchLastSection(True) self.d0_grid.setHorizontalHeaderLabels(['vx', 'vy', 'vz', "d₀", "Δd₀"]) spinBoxDelegate = SpinBoxDelegate() self.d0_grid.setItemDelegateForColumn(3, spinBoxDelegate) self.d0_grid.setItemDelegateForColumn(4, spinBoxDelegate) layout.addWidget(self.d0_grid) self.setLayout(layout) self.set_case('Constant') def set_case(self, case): if case == "Constant": self.d0_box.setEnabled(True) self.load_grid.setEnabled(False) self.save_grid.setEnabled(False) self.d0_grid.setEnabled(False) else: self.d0_box.setEnabled(False) self.load_grid.setEnabled(True) self.save_grid.setEnabled(True) self.d0_grid.setEnabled(True) def update_d0(self): self._parent.update_plot() def set_d0(self, d0, d0e): if d0 is None: self.d0.clear() self.d0e.clear() else: self.d0.setText(str(d0)) self.d0e.setText(str(d0e)) def set_d0_field(self, x, y, z, d0, d0e): if x is None: self.d0_grid.clearContents() else: self.d0_grid.setRowCount(len(x)) for n in range(len(x)): x_item = QTableWidgetItem(f'{x[n]: 7.2f}') x_item.setFlags(x_item.flags() ^ Qt.ItemIsEditable) y_item = QTableWidgetItem(f'{y[n]: 7.2f}') y_item.setFlags(y_item.flags() ^ Qt.ItemIsEditable) z_item = QTableWidgetItem(f'{z[n]: 7.2f}') z_item.setFlags(z_item.flags() ^ Qt.ItemIsEditable) d0_item = QTableWidgetItem() d0_item.setData(Qt.EditRole, float(d0[n])) d0e_item = QTableWidgetItem() d0e_item.setData(Qt.EditRole, float(d0e[n])) self.d0_grid.setItem(n, 0, QTableWidgetItem(x_item)) self.d0_grid.setItem(n, 1, QTableWidgetItem(y_item)) self.d0_grid.setItem(n, 2, QTableWidgetItem(z_item)) self.d0_grid.setItem(n, 3, QTableWidgetItem(d0_item)) self.d0_grid.setItem(n, 4, QTableWidgetItem(d0e_item)) def get_d0_field(self): if self.d0_grid.rowCount() == 0: return None else: x = [ float(self.d0_grid.item(row, 0).text()) for row in range(self.d0_grid.rowCount()) ] y = [ float(self.d0_grid.item(row, 1).text()) for row in range(self.d0_grid.rowCount()) ] z = [ float(self.d0_grid.item(row, 2).text()) for row in range(self.d0_grid.rowCount()) ] d0 = [ float(self.d0_grid.item(row, 3).text()) for row in range(self.d0_grid.rowCount()) ] d0e = [ float(self.d0_grid.item(row, 4).text()) for row in range(self.d0_grid.rowCount()) ] return (d0, d0e, x, y, z) def save_d0_field(self): filename, _ = QFileDialog.getSaveFileName( self, "Save d0 Grid", "", "CSV (*.csv);;All Files (*)") if filename: d0, d0e, x, y, z = self.get_d0_field() np.savetxt(filename, np.array([x, y, z, d0, d0e]).T, fmt=['%.4g', '%.4g', '%.4g', '%.9g', '%.9g'], header="vx, vy, vz, d0, d0_error", delimiter=',') def load_d0_field(self): filename, _ = QFileDialog.getOpenFileName( self, "Load d0 Grid", "", "CSV (*.csv);;All Files (*)") if filename: x, y, z, d0, d0e = np.loadtxt(filename, delimiter=',', unpack=True) self.set_d0_field(x, y, z, d0, d0e) def get_d0(self): if self.d0_grid_switch.currentText() == "Constant": try: return (float(self.d0.text()), float(self.d0e.text())) except ValueError: return None else: return self.get_d0_field()
class WndComputeRoiMaps(SecondaryWindow): # Signal that is sent (to main window) to update global state of the program update_global_state = Signal() computations_complete = Signal(object) signal_roi_computation_complete = Signal() signal_activate_tab_xrf_maps = Signal() def __init__(self, *, gpc, gui_vars): super().__init__() # Global processing classes self.gpc = gpc # Global GUI variables (used for control of GUI state) self.gui_vars = gui_vars # Reference to the main window. The main window will hold # references to all non-modal windows that could be opened # from multiple places in the program. self.ref_main_window = self.gui_vars["ref_main_window"] self.update_global_state.connect( self.ref_main_window.update_widget_state) self.initialize() def initialize(self): self.setWindowTitle("PyXRF: Compute XRF Maps Based on ROIs") self.setMinimumWidth(600) self.setMinimumHeight(300) self.resize(600, 600) header_vbox = self._setup_header() self._setup_table() footer_hbox = self._setup_footer() vbox = QVBoxLayout() vbox.addLayout(header_vbox) vbox.addWidget(self.table) vbox.addLayout(footer_hbox) self.setLayout(vbox) self._set_tooltips() def _setup_header(self): self.pb_clear = QPushButton("Clear") self.pb_clear.clicked.connect(self.pb_clear_clicked) self.pb_use_lines_for_fitting = QPushButton( "Use Lines Selected For Fitting") self.pb_use_lines_for_fitting.clicked.connect( self.pb_use_lines_for_fitting_clicked) self.le_sel_emission_lines = LineEditExtended() self.le_sel_emission_lines.textChanged.connect( self.le_sel_emission_lines_text_changed) self.le_sel_emission_lines.editingFinished.connect( self.le_sel_emission_lines_editing_finished) sample_elements = "" self.le_sel_emission_lines.setText(sample_elements) vbox = QVBoxLayout() hbox = QHBoxLayout() hbox.addWidget(QLabel("Enter emission lines, e.g. Fe_K, Gd_L ")) hbox.addStretch(1) hbox.addWidget(self.pb_clear) hbox.addWidget(self.pb_use_lines_for_fitting) vbox.addLayout(hbox) vbox.addWidget(self.le_sel_emission_lines) return vbox def _setup_table(self): # Labels for horizontal header self.tbl_labels = ["Line", "E, keV", "ROI, keV", "Show", "Reset"] # The list of columns that stretch with the table self.tbl_cols_stretch = ("E, keV", "ROI, keV") # Table item representation if different from default self.tbl_format = {"E, keV": ".3f"} # Editable items (highlighted with lighter background) self.tbl_cols_editable = {"ROI, keV"} # Columns that contain Range Manager self.tbl_cols_range_manager = ("ROI, keV", ) self.table = QTableWidget() self.table.setColumnCount(len(self.tbl_labels)) self.table.setHorizontalHeaderLabels(self.tbl_labels) self.table.verticalHeader().hide() self.table.setSelectionMode(QTableWidget.NoSelection) self.table.setStyleSheet("QTableWidget::item{color: black;}") header = self.table.horizontalHeader() for n, lbl in enumerate(self.tbl_labels): # Set stretching for the columns if lbl in self.tbl_cols_stretch: header.setSectionResizeMode(n, QHeaderView.Stretch) else: header.setSectionResizeMode(n, QHeaderView.ResizeToContents) self._table_contents = [] self.cb_list = [] self.range_manager_list = [] self.pb_default_list = [] self.fill_table(self._table_contents) def fill_table(self, table_contents): self.table.clearContents() self._table_contents = table_contents # Save new table contents for item in self.range_manager_list: item.selection_changed.disconnect( self.range_manager_selection_changed) self.range_manager_list = [] for cb in self.cb_list: cb.stateChanged.disconnect(self.cb_state_changed) self.cb_list = [] for pb in self.pb_default_list: pb.clicked.connect(self.pb_default_clicked) self.pb_default_list = [] self.table.setRowCount(len(table_contents)) for nr, row in enumerate(table_contents): eline_name = row["eline"] + "a1" energy = row["energy_center"] energy_left = row["energy_left"] energy_right = row["energy_right"] range_displayed = row["range_displayed"] table_row = [eline_name, energy, (energy_left, energy_right)] for nc, entry in enumerate(table_row): label = self.tbl_labels[nc] # Set alternating background colors for the table rows # Make background for editable items a little brighter brightness = 240 if label in self.tbl_cols_editable else 220 if nr % 2: rgb_bckg = (255, brightness, brightness) else: rgb_bckg = (brightness, 255, brightness) if self.tbl_labels[nc] not in self.tbl_cols_range_manager: if self.tbl_labels[nc] in self.tbl_format: fmt = self.tbl_format[self.tbl_labels[nc]] s = ("{:" + fmt + "}").format(entry) else: s = f"{entry}" item = QTableWidgetItem(s) if nc > 0: item.setTextAlignment(Qt.AlignCenter) else: item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter) # Set all columns not editable (unless needed) item.setFlags(item.flags() & ~Qt.ItemIsEditable) # Note, that there is no way to set style sheet for QTableWidgetItem item.setBackground(QBrush(QColor(*rgb_bckg))) self.table.setItem(nr, nc, item) else: spin_name = f"{nr}" item = RangeManager(name=spin_name, add_sliders=False, selection_to_range_min=0.0001) item.set_range( 0.0, 100.0) # The range is greater than needed (in keV) item.set_selection(value_low=entry[0], value_high=entry[1]) item.setTextColor((0, 0, 0)) # In case of dark theme item.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.range_manager_list.append(item) item.selection_changed.connect( self.range_manager_selection_changed) color = (rgb_bckg[0], rgb_bckg[1], rgb_bckg[2]) item.setBackground(color) self.table.setCellWidget(nr, nc, item) brightness = 220 if nr % 2: rgb_bckg = (255, brightness, brightness) else: rgb_bckg = (brightness, 255, brightness) item = QWidget() cb = CheckBoxNamed(name=f"{nr}") cb.setChecked(Qt.Checked if range_displayed else Qt.Unchecked) self.cb_list.append(cb) cb.stateChanged.connect(self.cb_state_changed) item_hbox = QHBoxLayout(item) item_hbox.addWidget(cb) item_hbox.setAlignment(Qt.AlignCenter) item_hbox.setContentsMargins(0, 0, 0, 0) color_css = f"rgb({rgb_bckg[0]}, {rgb_bckg[1]}, {rgb_bckg[2]})" item.setStyleSheet( f"QWidget {{ background-color: {color_css}; }} " f"QCheckBox {{ color: black; background-color: white }}") self.table.setCellWidget(nr, nc + 1, item) item = PushButtonNamed("Reset", name=f"{nr}") item.clicked.connect(self.pb_default_clicked) self.pb_default_list.append(item) rgb_bckg = [_ - 35 if (_ < 255) else _ for _ in rgb_bckg] color_css = f"rgb({rgb_bckg[0]}, {rgb_bckg[1]}, {rgb_bckg[2]})" item.setStyleSheet( f"QPushButton {{ color: black; background-color: {color_css}; }}" ) self.table.setCellWidget(nr, nc + 2, item) def _setup_footer(self): self.cb_subtract_baseline = QCheckBox("Subtract baseline") self.cb_subtract_baseline.setChecked( Qt.Checked if self.gpc.get_roi_subtract_background( ) else Qt.Unchecked) self.cb_subtract_baseline.toggled.connect( self.cb_subtract_baseline_toggled) self.pb_compute_roi = QPushButton("Compute ROIs") self.pb_compute_roi.clicked.connect(self.pb_compute_roi_clicked) hbox = QHBoxLayout() hbox.addWidget(self.cb_subtract_baseline) hbox.addStretch(1) hbox.addWidget(self.pb_compute_roi) return hbox def _set_tooltips(self): set_tooltip(self.pb_clear, "<b>Clear</b> the list") set_tooltip( self.pb_use_lines_for_fitting, "Copy the contents of <b>the list of emission lines selected for fitting</b> to the list of ROIs", ) set_tooltip( self.le_sel_emission_lines, "The list of <b>emission lines</b> selected for ROI computation.") set_tooltip(self.table, "The list of ROIs") set_tooltip( self.cb_subtract_baseline, "<b>Subtract baseline</b> from the pixel spectra before computing ROIs. " "Subtracting baseline slows down computations and usually have no benefit. " "In most cases it should remain <b>unchecked</b>.", ) set_tooltip( self.pb_compute_roi, "<b>Run</b> computations of the ROIs. The resulting <b>ROI</b> dataset " "may be viewed in <b>XRF Maps</b> tab.", ) def update_widget_state(self, condition=None): # Update the state of the menu bar state = not self.gui_vars["gui_state"]["running_computations"] self.setEnabled(state) # Hide the window if required by the program state state_file_loaded = self.gui_vars["gui_state"]["state_file_loaded"] state_model_exist = self.gui_vars["gui_state"]["state_model_exists"] if not state_file_loaded or not state_model_exist: self.hide() if condition == "tooltips": self._set_tooltips() def pb_clear_clicked(self): self.gpc.clear_roi_element_list() self._update_displayed_element_list() self._validate_element_list() def pb_use_lines_for_fitting_clicked(self): self.gpc.load_roi_element_list_from_selected() self._update_displayed_element_list() self._validate_element_list() def le_sel_emission_lines_text_changed(self, text): self._validate_element_list(text) def le_sel_emission_lines_editing_finished(self): text = self.le_sel_emission_lines.text() if self._validate_element_list(text): self.gpc.set_roi_selected_element_list(text) self._update_table() else: element_list = self.gpc.get_roi_selected_element_list() self.le_sel_emission_lines.setText(element_list) def cb_subtract_baseline_toggled(self, state): self.gpc.set_roi_subtract_background(bool(state)) def cb_state_changed(self, name, state): try: nr = int(name) # Row number checked = state == Qt.Checked eline = self._table_contents[nr]["eline"] self._table_contents[nr]["range_displayed"] = checked self.gpc.show_roi(eline, checked) except Exception as ex: logger.error( f"Failed to process selection change. Exception occurred: {ex}." ) def _find_spin_box(self, name): for item in self.spin_list: if item.getName() == name: return item return None def spin_value_changed(self, name, value): try: nr, side = name.split(",") nr = int(nr) keys = {"left": "energy_left", "right": "energy_right"} side = keys[side] eline = self._table_contents[nr]["eline"] if self._table_contents[nr][side] == value: return if side == "energy_left": # Left boundary if value < self._table_contents[nr]["energy_right"]: self._table_contents[nr][side] = value else: # Right boundary if value > self._table_contents[nr]["energy_left"]: self._table_contents[nr][side] = value # Update plot left, right = self._table_contents[nr][ "energy_left"], self._table_contents[nr]["energy_right"] self.gpc.change_roi(eline, left, right) except Exception as ex: logger.error( f"Failed to change the ROI. Exception occurred: {ex}.") def range_manager_selection_changed(self, left, right, name): try: nr = int(name) eline = self._table_contents[nr]["eline"] self.gpc.change_roi(eline, left, right) except Exception as ex: logger.error( f"Failed to change the ROI. Exception occurred: {ex}.") def pb_default_clicked(self, name): try: nr = int(name) eline = self._table_contents[nr]["eline"] left = self._table_contents[nr]["energy_left_default"] right = self._table_contents[nr]["energy_right_default"] self.range_manager_list[nr].set_selection(value_low=left, value_high=right) self.gpc.change_roi(eline, left, right) except Exception as ex: logger.error( f"Failed to change the ROI. Exception occurred: {ex}.") def pb_compute_roi_clicked(self): def cb(): try: self.gpc.compute_rois() success, msg = True, "" except Exception as ex: success, msg = False, str(ex) return {"success": success, "msg": msg} self._compute_in_background(cb, self.slot_compute_roi_clicked) @Slot(object) def slot_compute_roi_clicked(self, result): self._recover_after_compute(self.slot_compute_roi_clicked) success = result["success"] if success: self.gui_vars["gui_state"]["state_xrf_map_exists"] = True else: msg = result["msg"] msgbox = QMessageBox(QMessageBox.Critical, "Failed to Compute ROIs", msg, QMessageBox.Ok, parent=self) msgbox.exec() self.signal_roi_computation_complete.emit() self.update_global_state.emit() if success: self.signal_activate_tab_xrf_maps.emit() def _update_displayed_element_list(self): element_list = self.gpc.get_roi_selected_element_list() self.le_sel_emission_lines.setText(element_list) self._validate_element_list() self._update_table() def _update_table(self): table_contents = self.gpc.get_roi_settings() self.fill_table(table_contents) def _validate_element_list(self, text=None): if text is None: text = self.le_sel_emission_lines.text() el_list = text.split(",") el_list = [_.strip() for _ in el_list] if el_list == [""]: el_list = [] valid = bool(len(el_list)) for eline in el_list: if self.gpc.get_eline_name_category(eline) != "eline": valid = False self.le_sel_emission_lines.setValid(valid) self.pb_compute_roi.setEnabled(valid) return valid def _compute_in_background(self, func, slot, *args, **kwargs): """ Run function `func` in a background thread. Send the signal `self.computations_complete` once computation is finished. Parameters ---------- func: function Reference to a function that is supposed to be executed at the background. The function return value is passed as a signal parameter once computation is complete. slot: qtpy.QtCore.Slot or None Reference to a slot. If not None, then the signal `self.computation_complete` is connected to this slot. args, kwargs arguments of the function `func`. """ signal_complete = self.computations_complete def func_to_run(func, *args, **kwargs): class LoadFile(QRunnable): def run(self): result_dict = func(*args, **kwargs) signal_complete.emit(result_dict) return LoadFile() if slot is not None: self.computations_complete.connect(slot) self.gui_vars["gui_state"]["running_computations"] = True self.update_global_state.emit() QThreadPool.globalInstance().start(func_to_run(func, *args, **kwargs)) def _recover_after_compute(self, slot): """ The function should be called after the signal `self.computations_complete` is received. The slot should be the same as the one used when calling `self.compute_in_background`. """ if slot is not None: self.computations_complete.disconnect(slot) self.gui_vars["gui_state"]["running_computations"] = False self.update_global_state.emit()