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 = EnumComboBox(Units) self.units_select.set_value(self.settings.get("simple_measurements.units", Units.nm)) self.units_select.currentIndexChanged.connect(self.change_units) 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: try: geometry = self.settings.get_from_profile("simple_measurement_window_geometry") self.restoreGeometry(QByteArray.fromHex(bytes(geometry, "ascii"))) except KeyError: pass 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.segmentation is None: QMessageBox.warning(self, "No segmentation", "need segmentation to work") return to_calculate = [] for i in range(2, 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={ "channel": self.settings.image.get_channel(self.channel_select.get_value()), "segmentation": self.settings.segmentation, "mask": None, "voxel_size": self.settings.image.spacing, "result_units": self.units_select.get_value(), }, ) 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() - 2): # noinspection PyTypeChecker chk: QCheckBox = self.measurement_layout.takeAt(2).widget() if chk.isChecked(): selected.add(chk.text()) chk.deleteLater() return selected def event(self, event: QEvent) -> bool: if event.type() == QEvent.WindowActivate: self.channel_select.change_channels_num(self.settings.image.channels) 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) return super().event(event) def change_units(self): self.settings.set("simple_measurements.units", self.units_select.get_value())
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 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 LogViewerDialog(DialogBase): """Logger widget.""" def __init__( self, parent=None, log_folder=LOG_FOLDER, log_filename=LOG_FILENAME, ): """ Logger widget. Parameters ---------- log_folder: str Folder where logs are located log_filename: str Basic name for the rotating log files. """ super(LogViewerDialog, self).__init__(parent=parent) self._data = None self._columns = ['level', 'time', 'module', 'method', 'message'] self._headers = [c.capitalize() for c in self._columns] self._log_filename = log_filename self._log_folder = log_folder # Widgets self.label = QLabel('Select log file:') self.combobox = ComboBoxBase() self.table_logs = QTableWidget(self) self.button_copy = ButtonPrimary('Copy') self.text_search = LineEditSearch() # Widget setup self.table_logs.setAttribute(Qt.WA_LayoutUsesWidgetRect, True) horizontal_header = self.table_logs.horizontalHeader() vertical_header = self.table_logs.verticalHeader() horizontal_header.setStretchLastSection(True) horizontal_header.setSectionResizeMode(QHeaderView.Fixed) vertical_header.setSectionResizeMode(QHeaderView.Fixed) self.table_logs.setSelectionBehavior(QTableWidget.SelectRows) self.table_logs.setEditTriggers(QTableWidget.NoEditTriggers) self.setWindowTitle('Log Viewer') self.setMinimumWidth(800) self.setMinimumHeight(500) self.text_search.setPlaceholderText("Search...") # Layouts top_layout = QHBoxLayout() top_layout.addWidget(self.label) top_layout.addWidget(SpacerHorizontal()) top_layout.addWidget(self.combobox) top_layout.addStretch() top_layout.addWidget(SpacerHorizontal()) top_layout.addWidget(self.text_search) top_layout.addWidget(SpacerHorizontal()) top_layout.addWidget(self.button_copy) layout = QVBoxLayout() layout.addLayout(top_layout) layout.addWidget(SpacerVertical()) layout.addWidget(self.table_logs) self.setLayout(layout) # Signals self.combobox.currentIndexChanged.connect(self.update_text) self.button_copy.clicked.connect(self.copy_item) self.text_search.textChanged.connect(self.filter_text) # Setup() self.setup() self.update_style_sheet() def update_style_sheet(self, style_sheet=None): """Update custom CSS stylesheet.""" if style_sheet is None: style_sheet = load_style_sheet() self.setStyleSheet(style_sheet) def setup(self): """Setup widget content.""" self.combobox.clear() paths = log_files( log_folder=self._log_folder, log_filename=self._log_filename, ) files = [os.path.basename(p) for p in paths] self.combobox.addItems(files) def filter_text(self): """Search for text in the selected log file.""" search = self.text_search.text().lower() for i, data in enumerate(self._data): if any(search in str(d).lower() for d in data.values()): self.table_logs.showRow(i) else: self.table_logs.hideRow(i) def row_data(self, row): """Give the current row data concatenated with spaces.""" data = {} if self._data: length = len(self._data) if 0 >= row < length: data = self._data[row] return data def update_text(self, index): """Update logs based on combobox selection.""" path = os.path.join(self._log_folder, self.combobox.currentText()) self._data = load_log(path) self.table_logs.clear() self.table_logs.setSortingEnabled(False) self.table_logs.setRowCount(len(self._data)) self.table_logs.setColumnCount(len(self._columns)) self.table_logs.setHorizontalHeaderLabels(self._headers) for row, data in enumerate(self._data): for col, col_key in enumerate(self._columns): item = QTableWidgetItem(data.get(col_key, '')) self.table_logs.setItem(row, col, item) for c in [0, 2, 3]: self.table_logs.resizeColumnToContents(c) self.table_logs.resizeRowsToContents() self.table_logs.setSortingEnabled(True) self.table_logs.scrollToBottom() self.table_logs.scrollToTop() self.table_logs.sortByColumn(1, Qt.AscendingOrder) # Make sure there is always a selected row self.table_logs.setCurrentCell(0, 0) def copy_item(self): """Copy selected item to clipboard in markdown format.""" app = QApplication.instance() items = self.table_logs.selectedIndexes() if items: rows = set(sorted(i.row() for i in items)) if self._data: all_data = [self._data[row] for row in rows] dump = json.dumps(all_data, sort_keys=True, indent=4) app.clipboard().setText('```json\n' + dump + '\n```')
class WndLoadQuantitativeCalibration(SecondaryWindow): signal_quantitative_calibration_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 self.initialize() def initialize(self): self.table_header_display_names = False self.setWindowTitle("PyXRF: Load Quantitative Calibration") self.setMinimumWidth(750) self.setMinimumHeight(400) self.resize(750, 600) self.pb_load_calib = QPushButton("Load Calibration ...") self.pb_load_calib.clicked.connect(self.pb_load_calib_clicked) self._changes_exist = False self._auto_update = True self.cb_auto_update = QCheckBox("Auto") self.cb_auto_update.setCheckState( Qt.Checked if self._auto_update else Qt.Unchecked) self.cb_auto_update.stateChanged.connect( self.cb_auto_update_state_changed) self.pb_update_plots = QPushButton("Update Plots") self.pb_update_plots.clicked.connect(self.pb_update_plots_clicked) self.grp_current_scan = QGroupBox( "Parameters of Currently Processed Scan") self._distance_to_sample = 0.0 self.le_distance_to_sample = LineEditExtended() le_dist_validator = QDoubleValidator() le_dist_validator.setBottom(0) self.le_distance_to_sample.setValidator(le_dist_validator) self._set_distance_to_sample() self.le_distance_to_sample.editingFinished.connect( self.le_distance_to_sample_editing_finished) self.le_distance_to_sample.focusOut.connect( self.le_distance_to_sample_focus_out) hbox = QHBoxLayout() hbox.addWidget(QLabel("Distance-to-sample:")) hbox.addWidget(self.le_distance_to_sample) hbox.addStretch(1) self.grp_current_scan.setLayout(hbox) self.eline_rb_exclusive = [ ] # Holds the list of groups of exclusive radio buttons self._setup_tab_widget() vbox = QVBoxLayout() hbox = QHBoxLayout() hbox.addWidget(self.pb_load_calib) hbox.addStretch(1) hbox.addWidget(self.cb_auto_update) hbox.addWidget(self.pb_update_plots) vbox.addLayout(hbox) vbox.addWidget(self.tab_widget) vbox.addWidget(self.grp_current_scan) self.setLayout(vbox) # Display data self.update_all_data() self._set_tooltips() def _setup_tab_widget(self): self.tab_widget = QTabWidget() self.loaded_standards = QWidget() # self.display_loaded_standards() self.scroll = QScrollArea() self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.scroll.setWidget(self.loaded_standards) self.tab_widget.addTab(self.scroll, "Loaded Standards") self.combo_set_table_header = QComboBox() self.combo_set_table_header.addItems( ["Standard Serial #", "Standard Name"]) self.combo_set_table_header.currentIndexChanged.connect( self.combo_set_table_header_index_changed) vbox = QVBoxLayout() vbox.addSpacing(5) hbox = QHBoxLayout() hbox.addWidget(QLabel("Display in table header:")) hbox.addWidget(self.combo_set_table_header) hbox.addStretch(1) vbox.addLayout(hbox) self.table = QTableWidget() self.table.verticalHeader().hide() self.table.setSelectionMode(QTableWidget.NoSelection) self.table.horizontalHeader().setSectionResizeMode( QHeaderView.ResizeToContents) self.table.horizontalHeader().setMinimumSectionSize(150) vbox.addWidget(self.table) self.table.setStyleSheet("QTableWidget::item{color: black;}") frame = QFrame() vbox.setContentsMargins(0, 0, 0, 0) frame.setLayout(vbox) self.tab_widget.addTab(frame, "Selected Emission Lines") def display_loaded_standards(self): calib_data = self.gpc.get_quant_calibration_data() calib_settings = self.gpc.get_quant_calibration_settings() # Create the new widget (this deletes the old widget) self.loaded_standards = QWidget() self.loaded_standards.setMinimumWidth(700) # Also delete references to all components self.frames_calib_data = [] self.pbs_view = [] self.pbs_remove = [] # All 'View' buttons are added to the group in order to be connected to the same slot self.group_view = QButtonGroup() self.group_view.setExclusive(False) self.group_view.buttonClicked.connect(self.pb_view_clicked) # The same for the 'Remove' buttons self.group_remove = QButtonGroup() self.group_remove.setExclusive(False) self.group_remove.buttonClicked.connect(self.pb_remove_clicked) vbox = QVBoxLayout() class _LabelBlack(QLabel): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setStyleSheet("color: black") for cdata, csettings in zip(calib_data, calib_settings): frame = QFrame() frame.setFrameStyle(QFrame.StyledPanel) frame.setStyleSheet( get_background_css((200, 255, 200), widget="QFrame")) _vbox = QVBoxLayout() name = cdata["name"] # Standard name (can be arbitrary string # If name is long, then print it in a separate line _name_is_long = len(name) > 30 pb_view = QPushButton("View ...") self.group_view.addButton(pb_view) pb_remove = QPushButton("Remove") self.group_remove.addButton(pb_remove) # Row 1: serial, name serial = cdata["serial"] _hbox = QHBoxLayout() _hbox.addWidget(_LabelBlack(f"<b>Standard</b> #{serial}")) if not _name_is_long: _hbox.addWidget(_LabelBlack(f"'{name}'")) _hbox.addStretch(1) _hbox.addWidget(pb_view) _hbox.addWidget(pb_remove) _vbox.addLayout(_hbox) # Optional row if _name_is_long: # Wrap name if it is extemely long name = textwrap.fill(name, width=80) _hbox = QHBoxLayout() _hbox.addWidget(_LabelBlack("<b>Name:</b> "), 0, Qt.AlignTop) _hbox.addWidget(_LabelBlack(name), 0, Qt.AlignTop) _hbox.addStretch(1) _vbox.addLayout(_hbox) # Row 2: description description = textwrap.fill(cdata["description"], width=80) _hbox = QHBoxLayout() _hbox.addWidget(_LabelBlack("<b>Description:</b>"), 0, Qt.AlignTop) _hbox.addWidget(_LabelBlack(f"{description}"), 0, Qt.AlignTop) _hbox.addStretch(1) _vbox.addLayout(_hbox) # Row 3: incident_energy = cdata["incident_energy"] scaler = cdata["scaler_name"] detector_channel = cdata["detector_channel"] distance_to_sample = cdata["distance_to_sample"] _hbox = QHBoxLayout() _hbox.addWidget( _LabelBlack(f"<b>Incident energy, keV:</b> {incident_energy}")) _hbox.addWidget(_LabelBlack(f" <b>Scaler:</b> {scaler}")) _hbox.addWidget( _LabelBlack(f" <b>Detector channel:</b> {detector_channel}")) _hbox.addWidget( _LabelBlack( f" <b>Distance-to-sample:</b> {distance_to_sample}")) _hbox.addStretch(1) _vbox.addLayout(_hbox) # Row 4: file name fln = textwrap.fill(csettings["file_path"], width=80) _hbox = QHBoxLayout() _hbox.addWidget(_LabelBlack("<b>Source file:</b>"), 0, Qt.AlignTop) _hbox.addWidget(_LabelBlack(fln), 0, Qt.AlignTop) _hbox.addStretch(1) _vbox.addLayout(_hbox) frame.setLayout(_vbox) # Now the group box is added to the upper level layout vbox.addWidget(frame) vbox.addSpacing(5) self.frames_calib_data.append(frame) self.pbs_view.append(pb_view) self.pbs_remove.append(pb_remove) # Add the layout to the widget self.loaded_standards.setLayout(vbox) # ... and put the widget inside the scroll area. This will update the # contents of the scroll area. self.scroll.setWidget(self.loaded_standards) def display_table_header(self): calib_data = self.gpc.get_quant_calibration_data() header_by_name = self.table_header_display_names tbl_labels = ["Lines"] for n, cdata in enumerate(calib_data): if header_by_name: txt = cdata["name"] else: txt = cdata["serial"] txt = textwrap.fill(txt, width=20) tbl_labels.append(txt) self.table.setHorizontalHeaderLabels(tbl_labels) def display_standard_selection_table(self): calib_data = self.gpc.get_quant_calibration_data() self._quant_file_paths = self.gpc.get_quant_calibration_file_path_list( ) brightness = 220 table_colors = [(255, brightness, brightness), (brightness, 255, brightness)] # Disconnect all radio button signals before clearing the table for bgroup in self.eline_rb_exclusive: bgroup.buttonToggled.disconnect(self.rb_selection_toggled) # This list will hold radio button groups for horizontal rows # Those are exclusive groups. They are not going to be # used directly, but they must be kept alive in order # for the radiobuttons to work properly. Most of the groups # will contain only 1 radiobutton, which will always remain checked. self.eline_rb_exclusive = [] # The following list will contain the list of radio buttons for each # row. If there is no radiobutton in a position, then the element is # set to None. # N rows: the number of emission lines, N cols: the number of standards self.eline_rb_lists = [] self.table.clear() if not calib_data: self.table.setRowCount(0) self.table.setColumnCount(0) else: # Create the sorted list of available element lines line_set = set() for cdata in calib_data: ks = list(cdata["element_lines"].keys()) line_set.update(list(ks)) self.eline_list = list(line_set) self.eline_list.sort() for n in range(len(self.eline_list)): self.eline_rb_exclusive.append(QButtonGroup()) self.eline_rb_lists.append([None] * len(calib_data)) self.table.setColumnCount(len(calib_data) + 1) self.table.setRowCount(len(self.eline_list)) self.display_table_header() for n, eline in enumerate(self.eline_list): rgb = table_colors[n % 2] item = QTableWidgetItem(eline) item.setTextAlignment(Qt.AlignCenter) item.setFlags(item.flags() & ~Qt.ItemIsEditable) item.setBackground(QBrush(QColor(*rgb))) self.table.setItem(n, 0, item) for ns, cdata in enumerate(calib_data): q_file_path = self._quant_file_paths[ ns] # Used to identify standard if eline in cdata["element_lines"]: rb = QRadioButton() if self.gpc.get_quant_calibration_is_eline_selected( eline, q_file_path): rb.setChecked(True) rb.setStyleSheet("color: black") self.eline_rb_lists[n][ns] = rb # self.eline_rb_by_standard[ns].addButton(rb) self.eline_rb_exclusive[n].addButton(rb) item = QWidget() item_hbox = QHBoxLayout(item) item_hbox.addWidget(rb) item_hbox.setAlignment(Qt.AlignCenter) item_hbox.setContentsMargins(0, 0, 0, 0) item.setStyleSheet(get_background_css(rgb)) # Generate tooltip density = cdata["element_lines"][eline]["density"] fluorescence = cdata["element_lines"][eline][ "fluorescence"] ttip = f"Fluorescence (F): {fluorescence:12g}\nDensity (D): {density:12g}\n" # Avoid very small values of density (probably zero) if abs(density) > 1e-30: ttip += f"F/D: {fluorescence/density:12g}" item.setToolTip(ttip) self.table.setCellWidget(n, ns + 1, item) else: # There is no radio button, but we still need to fill the cell item = QTableWidgetItem("") item.setFlags(item.flags() & ~Qt.ItemIsEditable) item.setBackground(QBrush(QColor(*rgb))) self.table.setItem(n, ns + 1, item) # Now the table is set (specifically radio buttons). # So we can connect the button groups with the event processing function for bgroup in self.eline_rb_exclusive: bgroup.buttonToggled.connect(self.rb_selection_toggled) @Slot() def update_all_data(self): self.display_loaded_standards() self.display_standard_selection_table() self._set_distance_to_sample() def _set_distance_to_sample(self): """Set 'le_distance_to_sample` without updating maps""" distance_to_sample = self.gpc.get_quant_calibration_distance_to_sample( ) if distance_to_sample is None: distance_to_sample = 0.0 self._distance_to_sample = distance_to_sample self._set_le_distance_to_sample(distance_to_sample) def _set_tooltips(self): set_tooltip(self.pb_load_calib, "Load <b>calibration data</b> from JSON file.") set_tooltip( self.cb_auto_update, "Automatically <b>update the plots</b> when changes are made. " "If unchecked, then button <b>Update Plots</b> must be pressed " "to update the plots. Automatic update is often undesirable " "when large maps are displayed and multiple changes to parameters " "are made.", ) set_tooltip( self.pb_update_plots, "<b>Update plots</b> based on currently selected parameters.") set_tooltip( self.le_distance_to_sample, "Distance between <b>the sample and the detector</b>. The ratio between of the distances " "during calibration and measurement is used to scale computed concentrations. " "If distance-to-sample is 0 for calibration or measurement, then no scaling is performed.", ) set_tooltip( self.combo_set_table_header, "Use <b>Serial Number</b> or <b>Name</b> of the calibration standard in the header of the table", ) set_tooltip( self.table, "Use Radio Buttons to select the <b>source of calibration data</b> for each emission line. " "This feature is needed if multiple loaded calibration files have data on the same " "emission line.", ) 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_xrf_map_exists = self.gui_vars["gui_state"][ "state_xrf_map_exists"] if not state_xrf_map_exists: self.hide() if condition == "tooltips": self._set_tooltips() def cb_auto_update_state_changed(self, state): self._auto_update = state self.pb_update_plots.setEnabled(not state) # If changes were made, apply the changes while switching to 'auto' mode if state and self._changes_exist: self._update_maps_auto() def pb_update_plots_clicked(self): self._update_maps() def pb_load_calib_clicked(self): current_dir = self.gpc.get_current_working_directory() file_name = QFileDialog.getOpenFileName( self, "Select File with Quantitative Calibration Data", current_dir, "JSON (*.json);; All (*)") file_name = file_name[0] if file_name: try: logger.debug( f"Loading quantitative calibration from file: '{file_name}'" ) self.gpc.load_quantitative_calibration_data(file_name) self.update_all_data() self._update_maps_auto() except Exception: msg = "The selected JSON file has incorrect format. Select a different file." msgbox = QMessageBox(QMessageBox.Critical, "Data Loading Error", msg, QMessageBox.Ok, parent=self) msgbox.exec() def pb_view_clicked(self, button): try: n_standard = self.pbs_view.index(button) calib_settings = self.gpc.get_quant_calibration_settings() file_path = calib_settings[n_standard]["file_path"] calib_preview = self.gpc.get_quant_calibration_text_preview( file_path) dlg = DialogViewCalibStandard(None, file_path=file_path, calib_preview=calib_preview) dlg.exec() except ValueError: logger.error( "'View' button was pressed, but not found in the list of buttons" ) def pb_remove_clicked(self, button): try: n_standard = self.pbs_remove.index(button) calib_settings = self.gpc.get_quant_calibration_settings() file_path = calib_settings[n_standard]["file_path"] self.gpc.quant_calibration_remove_entry(file_path) self.update_all_data() self._update_maps_auto() except ValueError: logger.error( "'Remove' button was pressed, but not found in the list of buttons" ) def rb_selection_toggled(self, button, checked): if checked: # Find the button in 2D list 'self.eline_rb_lists' button_found = False for nr, rb_list in enumerate(self.eline_rb_lists): try: nc = rb_list.index(button) button_found = True break except ValueError: pass if button_found: eline = self.eline_list[nr] n_standard = nc file_path = self._quant_file_paths[n_standard] self.gpc.set_quant_calibration_select_eline(eline, file_path) self._update_maps_auto() else: # This should never happen logger.error( "Selection radio button was pressed, but not found in the list" ) def combo_set_table_header_index_changed(self, index): self.table_header_display_names = bool(index) self.display_table_header() def le_distance_to_sample_editing_finished(self): distance_to_sample = float(self.le_distance_to_sample.text()) if distance_to_sample != self._distance_to_sample: self._distance_to_sample = distance_to_sample self.gpc.set_quant_calibration_distance_to_sample( distance_to_sample) self._update_maps_auto() def le_distance_to_sample_focus_out(self): try: float(self.le_distance_to_sample.text()) except ValueError: # If the text can not be interpreted to float, then replace the text with the old value self._set_le_distance_to_sample(self._distance_to_sample) def _set_le_distance_to_sample(self, distance_to_sample): self.le_distance_to_sample.setText(f"{distance_to_sample:.12g}") def _update_maps_auto(self): """Update maps only if 'auto' update is ON. Used as a 'filter' to prevent extra plot updates.""" self._changes_exist = True if self._auto_update: self._update_maps() def _update_maps(self): """Upload the selections (limit table) and update plot""" self._changes_exist = False self._redraw_maps() # Emit signal only after the maps are redrawn. This should change # ranges in the respective controls for the plots self.signal_quantitative_calibration_changed.emit() def _redraw_maps(self): # We don't emit any signals here, but we don't really need to. logger.debug("Redrawing RGB XRF Maps") self.gpc.compute_map_ranges() self.gpc.redraw_maps() self.gpc.compute_rgb_map_ranges() self.gpc.redraw_rgb_maps()