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 MeasurementWidget(QWidget): """ :type settings: Settings :type segment: Segment """ def __init__(self, settings: PartSettings, segment=None): super().__init__() self.settings = settings self.segment = segment self.measurements_storage = MeasurementsStorage() self.recalculate_button = QPushButton( "Recalculate and\n replace measurement", self) self.recalculate_button.clicked.connect( self.replace_measurement_result) self.recalculate_append_button = QPushButton( "Recalculate and\n append measurement", self) self.recalculate_append_button.clicked.connect( self.append_measurement_result) self.copy_button = QPushButton("Copy to clipboard", self) self.copy_button.setToolTip( "You cacn copy also with 'Ctrl+C'. To get raw copy copy with 'Ctrl+Shit+C'" ) self.horizontal_measurement_present = QCheckBox( "Horizontal view", self) self.no_header = QCheckBox("No header", self) self.no_units = QCheckBox("No units", self) self.no_units.setChecked(True) self.no_units.clicked.connect(self.refresh_view) self.expand_mode = QCheckBox("Expand", self) self.expand_mode.setToolTip( "Shows results for each component in separate entry") self.file_names = EnumComboBox(FileNamesEnum) self.file_names_label = QLabel("Add file name:") self.file_names.currentIndexChanged.connect(self.refresh_view) self.horizontal_measurement_present.stateChanged.connect( self.refresh_view) self.expand_mode.stateChanged.connect(self.refresh_view) self.copy_button.clicked.connect(self.copy_to_clipboard) self.measurement_type = SearchCombBox(self) # noinspection PyUnresolvedReferences self.measurement_type.currentIndexChanged.connect( self.measurement_profile_selection_changed) self.measurement_type.addItem("<none>") self.measurement_type.addItems( list(sorted(self.settings.measurement_profiles.keys()))) self.measurement_type.setToolTip( 'You can create new measurement profile in advanced window, in tab "Measurement settings"' ) self.channels_chose = ChannelComboBox() self.units_choose = EnumComboBox(Units) self.units_choose.set_value(self.settings.get("units_value", Units.nm)) self.info_field = QTableWidget(self) self.info_field.setColumnCount(3) self.info_field.setHorizontalHeaderLabels(["Name", "Value", "Units"]) self.measurement_add_shift = 0 layout = QVBoxLayout() # layout.addWidget(self.recalculate_button) v_butt_layout = QVBoxLayout() v_butt_layout.setSpacing(1) self.up_butt_layout = QHBoxLayout() self.up_butt_layout.addWidget(self.recalculate_button) self.up_butt_layout.addWidget(self.recalculate_append_button) self.butt_layout = QHBoxLayout() # self.butt_layout.setMargin(0) # self.butt_layout.setSpacing(10) self.butt_layout.addWidget(self.horizontal_measurement_present, 1) self.butt_layout.addWidget(self.no_header, 1) self.butt_layout.addWidget(self.no_units, 1) self.butt_layout.addWidget(self.expand_mode, 1) self.butt_layout.addWidget(self.file_names_label) self.butt_layout.addWidget(self.file_names, 1) self.butt_layout.addWidget(self.copy_button, 2) self.butt_layout2 = QHBoxLayout() self.butt_layout3 = QHBoxLayout() self.butt_layout3.addWidget(QLabel("Channel:")) self.butt_layout3.addWidget(self.channels_chose) self.butt_layout3.addWidget(QLabel("Units:")) self.butt_layout3.addWidget(self.units_choose) # self.butt_layout3.addWidget(QLabel("Noise removal:")) # self.butt_layout3.addWidget(self.noise_removal_method) self.butt_layout3.addWidget(QLabel("Measurement set:")) self.butt_layout3.addWidget(self.measurement_type, 2) v_butt_layout.addLayout(self.up_butt_layout) v_butt_layout.addLayout(self.butt_layout) v_butt_layout.addLayout(self.butt_layout2) v_butt_layout.addLayout(self.butt_layout3) layout.addLayout(v_butt_layout) # layout.addLayout(self.butt_layout) layout.addWidget(self.info_field) self.setLayout(layout) # noinspection PyArgumentList self.clip = QApplication.clipboard() self.settings.image_changed[int].connect(self.image_changed) self.previous_profile = None def check_if_measurement_can_be_calculated(self, name): if name == "<none>": return "<none>" profile: MeasurementProfile = self.settings.measurement_profiles.get( name) if profile.is_any_mask_measurement() and self.settings.mask is None: QMessageBox.information( self, "Need mask", "To use this measurement set please use data with mask loaded", QMessageBox.Ok) self.measurement_type.setCurrentIndex(0) return "<none>" if self.settings.roi is None: QMessageBox.information( self, "Need segmentation", 'Before calculating please create segmentation ("Execute" button)', QMessageBox.Ok, ) self.measurement_type.setCurrentIndex(0) return "<none>" return name def image_changed(self, channels_num): self.channels_chose.change_channels_num(channels_num) def measurement_profile_selection_changed(self, index): text = self.measurement_type.itemText(index) text = self.check_if_measurement_can_be_calculated(text) try: stat = self.settings.measurement_profiles[text] is_mask = stat.is_any_mask_measurement() disable = is_mask and (self.settings.mask is None) except KeyError: disable = True self.recalculate_button.setDisabled(disable) self.recalculate_append_button.setDisabled(disable) if disable: self.recalculate_button.setToolTip( "Measurement profile contains mask measurements when mask is not loaded" ) self.recalculate_append_button.setToolTip( "Measurement profile contains mask measurements when mask is not loaded" ) else: self.recalculate_button.setToolTip("") self.recalculate_append_button.setToolTip("") def copy_to_clipboard(self): s = "" for r in range(self.info_field.rowCount()): for c in range(self.info_field.columnCount()): try: s += str(self.info_field.item(r, c).text()) + "\t" except AttributeError: s += "\t" s = s[:-1] + "\n" # eliminate last '\t' self.clip.setText(s) def replace_measurement_result(self): self.measurements_storage.clear() self.previous_profile = "" self.append_measurement_result() def refresh_view(self): self.measurements_storage.set_expand(self.expand_mode.isChecked()) self.measurements_storage.set_show_units(not self.no_units.isChecked()) self.info_field.clear() save_orientation = self.horizontal_measurement_present.isChecked() columns, rows = self.measurements_storage.get_size(save_orientation) self.info_field.setColumnCount(columns) self.info_field.setRowCount(rows) self.info_field.setHorizontalHeaderLabels( self.measurements_storage.get_header(save_orientation)) self.info_field.setVerticalHeaderLabels( self.measurements_storage.get_rows(save_orientation)) for x in range(rows): for y in range(columns): self.info_field.setItem( x, y, QTableWidgetItem( self.measurements_storage.get_val_as_str( x, y, save_orientation))) if self.file_names.get_value() == FileNamesEnum.No: if save_orientation: self.info_field.removeColumn(0) else: self.info_field.removeRow(0) elif self.file_names.get_value() == FileNamesEnum.Short: if save_orientation: columns = 1 else: rows = 1 for x in range(rows): for y in range(columns): item = self.info_field.item(x, y) item.setText(os.path.basename(item.text())) self.info_field.setEditTriggers(QAbstractItemView.NoEditTriggers) def append_measurement_result(self): try: compute_class = self.settings.measurement_profiles[ self.measurement_type.currentText()] except KeyError: QMessageBox.warning( self, "Measurement profile not found", f"Measurement profile '{self.measurement_type.currentText()}' not found'", ) return if self.settings.roi is None: return units = self.units_choose.get_value() # FIXME find which errors should be displayed as warning # def exception_hook(exception): # QMessageBox.warning(self, "Calculation error", f"Error during calculation: {exception}") for num in compute_class.get_channels_num(): if num >= self.settings.image.channels: QMessageBox.warning( self, "Measurement error", "Cannot calculate this measurement because " f"image do not have channel {num+1}", ) return thread = ExecuteFunctionThread( compute_class.calculate, [ self.settings.image, self.channels_chose.currentIndex(), self.settings.roi_info, units ], ) dial = WaitingDialog( thread, "Measurement calculation") # , exception_hook=exception_hook) dial.exec() stat: MeasurementResult = thread.result if stat is None: return stat.set_filename(self.settings.image_path) self.measurements_storage.add_measurements(stat) self.previous_profile = compute_class.name self.refresh_view() def keyPressEvent(self, e: QKeyEvent): if e.modifiers() & Qt.ControlModifier: selected = self.info_field.selectedRanges() if e.key() == Qt.Key_C: # copy s = "" for r in range(selected[0].topRow(), selected[0].bottomRow() + 1): for c in range(selected[0].leftColumn(), selected[0].rightColumn() + 1): try: s += str(self.info_field.item(r, c).text()) + "\t" except AttributeError: s += "\t" s = s[:-1] + "\n" # eliminate last '\t' self.clip.setText(s) def update_measurement_list(self): self.measurement_type.blockSignals(True) available = list(sorted(self.settings.measurement_profiles.keys())) text = self.measurement_type.currentText() try: index = available.index(text) + 1 except ValueError: index = 0 self.measurement_type.clear() self.measurement_type.addItem("<none>") self.measurement_type.addItems(available) self.measurement_type.setCurrentIndex(index) self.measurement_type.blockSignals(False) def showEvent(self, _): self.update_measurement_list() def event(self, event: QEvent): if event.type() == QEvent.WindowActivate: self.update_measurement_list() return super().event(event) @staticmethod def _move_widgets(widgets_list: List[Tuple[QWidget, int]], layout1: QBoxLayout, layout2: QBoxLayout): for el in widgets_list: layout1.removeWidget(el[0]) layout2.addWidget(el[0], el[1]) def resizeEvent(self, _event: QResizeEvent) -> None: if self.width() < 800 and self.butt_layout2.count() == 0: self._move_widgets( [(self.file_names_label, 1), (self.file_names, 1), (self.copy_button, 2)], self.butt_layout, self.butt_layout2, ) elif self.width() > 800 and self.butt_layout2.count() != 0: self._move_widgets( [(self.file_names_label, 1), (self.file_names, 1), (self.copy_button, 2)], self.butt_layout2, self.butt_layout, )
class SimpleMeasurements(QWidget): def __init__(self, settings: StackSettings, parent=None): super().__init__(parent) self.settings = settings self.calculate_btn = QPushButton("Calculate") self.calculate_btn.clicked.connect(self.calculate) self.result_view = QTableWidget() self.channel_select = ChannelComboBox() self.units_select = QEnumComboBox(enum_class=Units) self.units_select.setCurrentEnum( self.settings.get("simple_measurements.units", Units.nm)) self.units_select.currentIndexChanged.connect(self.change_units) self._shift = 2 layout = QHBoxLayout() self.measurement_layout = QVBoxLayout() l1 = QHBoxLayout() l1.addWidget(QLabel("Units")) l1.addWidget(self.units_select) self.measurement_layout.addLayout(l1) l2 = QHBoxLayout() l2.addWidget(QLabel("Channel")) l2.addWidget(self.channel_select) self.measurement_layout.addLayout(l2) layout.addLayout(self.measurement_layout) result_layout = QVBoxLayout() result_layout.addWidget(self.result_view) result_layout.addWidget(self.calculate_btn) layout.addLayout(result_layout) self.setLayout(layout) self.setWindowTitle("Measurement") if self.window() == self: with suppress(KeyError): geometry = self.settings.get_from_profile( "simple_measurement_window_geometry") self.restoreGeometry( QByteArray.fromHex(bytes(geometry, "ascii"))) def closeEvent(self, event: QCloseEvent) -> None: """ Save geometry if widget is used as standalone window. """ if self.window() == self: self.settings.set_in_profile( "simple_measurement_window_geometry", self.saveGeometry().toHex().data().decode("ascii")) super().closeEvent(event) def calculate(self): if self.settings.roi is None: QMessageBox.warning(self, "No segmentation", "need segmentation to work") return to_calculate = [] for i in range(self._shift, self.measurement_layout.count()): # noinspection PyTypeChecker chk: QCheckBox = self.measurement_layout.itemAt(i).widget() if chk.isChecked(): leaf: Leaf = MEASUREMENT_DICT[chk.text()].get_starting_leaf() to_calculate.append( leaf.replace_(per_component=PerComponent.Yes, area=AreaType.ROI)) if not to_calculate: QMessageBox.warning(self, "No measurement", "Select at least one measurement") return profile = MeasurementProfile( "", [MeasurementEntry(x.name, x) for x in to_calculate]) dial = ExecuteFunctionDialog( profile.calculate, kwargs={ "image": self.settings.image, "channel_num": self.channel_select.get_value(), "roi": self.settings.roi_info, "result_units": self.units_select.currentEnum(), }, ) dial.exec_() result: MeasurementResult = dial.get_result() values = result.get_separated() labels = result.get_labels() self.result_view.clear() self.result_view.setColumnCount(len(values) + 1) self.result_view.setRowCount(len(labels)) for i, val in enumerate(labels): self.result_view.setItem(i, 0, QTableWidgetItem(val)) for j, values_list in enumerate(values): for i, val in enumerate(values_list): self.result_view.setItem(i, j + 1, QTableWidgetItem(str(val))) def _clean_measurements(self): selected = set() for _ in range(self.measurement_layout.count() - self._shift): # noinspection PyTypeChecker chk: QCheckBox = self.measurement_layout.takeAt( self._shift).widget() if chk.isChecked(): selected.add(chk.text()) chk.deleteLater() return selected def refresh_measurements(self): selected = self._clean_measurements() for val in MEASUREMENT_DICT.values(): area = val.get_starting_leaf().area pc = val.get_starting_leaf().per_component if (val.get_fields() or (area is not None and area != AreaType.ROI) or (pc is not None and pc != PerComponent.Yes)): continue text = val.get_name() chk = QCheckBox(text) if text in selected: chk.setChecked(True) self.measurement_layout.addWidget(chk) def keyPressEvent(self, e: QKeyEvent): if not e.modifiers() & Qt.ControlModifier: return selected = self.result_view.selectedRanges() if e.key() == Qt.Key_C: # copy s = "" for r in range(selected[0].topRow(), selected[0].bottomRow() + 1): for c in range(selected[0].leftColumn(), selected[0].rightColumn() + 1): try: s += str(self.result_view.item(r, c).text()) + "\t" except AttributeError: s += "\t" s = s[:-1] + "\n" # eliminate last '\t' QApplication.clipboard().setText(s) def event(self, event: QEvent) -> bool: if event.type() == QEvent.WindowActivate: if self.settings.image is not None: self.channel_select.change_channels_num( self.settings.image.channels) self.refresh_measurements() return super().event(event) def change_units(self): self.settings.set("simple_measurements.units", self.units_select.currentEnum())
class DialogSelectQuantStandard(QDialog): def __init__(self, parent=None): super().__init__(parent) self._qe_param_built_in = [] self._qe_param_custom = [] self._qe_standard_selected = None self._qe_param = [] # The list of all standards self._custom_label = [] # The list of booleans: True - the standard is custom, False -built-in self.setWindowTitle("Load Quantitative Standard") self.setMinimumHeight(500) self.setMinimumWidth(600) self.resize(600, 500) self.selected_standard_index = -1 labels = ("C", "Serial #", "Name", "Description") col_stretch = ( QHeaderView.ResizeToContents, QHeaderView.ResizeToContents, QHeaderView.ResizeToContents, QHeaderView.Stretch, ) self.table = QTableWidget() set_tooltip( self.table, "To <b> load the sta" "ndard</b>, double-click on the table row or " "select the table row and then click <b>Ok</b> button.", ) self.table.setMinimumHeight(200) self.table.setColumnCount(len(labels)) self.table.verticalHeader().hide() self.table.setHorizontalHeaderLabels(labels) self.table.setSelectionBehavior(QTableWidget.SelectRows) self.table.setSelectionMode(QTableWidget.SingleSelection) self.table.itemSelectionChanged.connect(self.item_selection_changed) self.table.itemDoubleClicked.connect(self.item_double_clicked) self.table.setStyleSheet( "QTableWidget::item{color: black;}" "QTableWidget::item:selected{background-color: red;}" "QTableWidget::item:selected{color: white;}" ) header = self.table.horizontalHeader() for n, col_stretch in enumerate(col_stretch): # Set stretching for the columns header.setSectionResizeMode(n, col_stretch) self.lb_info = QLabel() self.lb_info.setText("Column 'C': * means that the standard is user-defined.") button_box = QDialogButtonBox(QDialogButtonBox.Open | QDialogButtonBox.Cancel) button_box.button(QDialogButtonBox.Cancel).setDefault(True) button_box.accepted.connect(self.accept) button_box.rejected.connect(self.reject) self.pb_open = button_box.button(QDialogButtonBox.Open) self.pb_open.setEnabled(False) vbox = QVBoxLayout() vbox.addWidget(self.table) vbox.addWidget(self.lb_info) vbox.addWidget(button_box) self.setLayout(vbox) def _fill_table(self, table_contents): self.table.setRowCount(len(table_contents)) for nr, row in enumerate(table_contents): for nc, entry in enumerate(row): s = textwrap.fill(entry, width=40) item = QTableWidgetItem(s) item.setFlags(item.flags() & ~Qt.ItemIsEditable) if not nc: item.setTextAlignment(Qt.AlignHCenter | Qt.AlignVCenter) self.table.setItem(nr, nc, item) self.table.resizeRowsToContents() brightness = 220 for nr in range(self.table.rowCount()): for nc in range(self.table.columnCount()): self.table.item(nr, nc) if nr % 2: color = QColor(255, brightness, brightness) else: color = QColor(brightness, 255, brightness) self.table.item(nr, nc).setBackground(QBrush(color)) try: index = self._qe_param.index(self._qe_standard_selected) self.selected_standard_index = index n_columns = self.table.columnCount() self.table.setRangeSelected(QTableWidgetSelectionRange(index, 0, index, n_columns - 1), True) except ValueError: pass def item_selection_changed(self): sel_ranges = self.table.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: self.selected_standard_index = sel_ranges[0].topRow() self.pb_open.setEnabled(True) else: self.selected_standard_index = -1 self.pb_open.setEnabled(False) def item_double_clicked(self): self.accept() def set_standards(self, qe_param_built_in, qe_param_custom, qe_standard_selected): self._qe_standard_selected = qe_standard_selected self._qe_param = qe_param_custom + qe_param_built_in custom_label = [True] * len(qe_param_custom) + [False] * len(qe_param_built_in) table_contents = [] for n, param in enumerate(self._qe_param): custom = "*" if custom_label[n] else "" serial = param["serial"] name = param["name"] description = param["description"] table_contents.append([custom, serial, name, description]) self._fill_table(table_contents) def get_selected_standard(self): if self.selected_standard_index >= 0: return self._qe_param[self.selected_standard_index] else: return None