def _add_tab(self, json=None): """will look at the json and will display the values in conflicts in a new tab to allow the user to fix the conflicts""" number_of_tabs = self.ui.tabWidget.count() _table = QTableWidget() # initialize each table columns_width = self._calculate_columns_width(json=json) for _col in np.arange(len(json[0])): _table.insertColumn(_col) _table.setColumnWidth(_col, columns_width[_col]) for _row in np.arange(len(json)): _table.insertRow(_row) self.list_table.append(_table) _table.setHorizontalHeaderLabels(self.columns_label) for _row in np.arange(len(json)): # run number _col = 0 list_runs = json[_row]["Run Number"] o_parser = ListRunsParser() checkbox = QRadioButton(o_parser.new_runs(list_runs=list_runs)) if _row == 0: checkbox.setChecked(True) # QtCore.QObject.connect(checkbox, QtCore.SIGNAL("clicked(bool)"), # lambda bool, row=_row, table_id=_table: # self._changed_conflict_checkbox(bool, row, table_id)) _table.setCellWidget(_row, _col, checkbox) _col += 1 # chemical formula item = QTableWidgetItem(json[_row]["chemical_formula"]) _table.setItem(_row, _col, item) _col += 1 # geometry item = QTableWidgetItem(json[_row]["geometry"]) _table.setItem(_row, _col, item) _col += 1 # mass_density item = QTableWidgetItem(json[_row]["mass_density"]) _table.setItem(_row, _col, item) _col += 1 # sample_env_device item = QTableWidgetItem(json[_row]["sample_env_device"]) _table.setItem(_row, _col, item) self.ui.tabWidget.insertTab(number_of_tabs, _table, "Conflict #{}".format(number_of_tabs))
def setup_ui(self): self.setLayout(QVBoxLayout()) bpm_table = QTableWidget(self) col_labels = ["X Pos (mm)", "Y Pos (mm)"] bpm_table.setColumnCount(len(col_labels)) bpm_table.setHorizontalHeaderLabels(col_labels) data = dev_list(self.start_marker, self.end_marker) bpm_names = [bpm['device'] for bpm in data] bpm_table.setRowCount(len(bpm_names)) bpm_table.setVerticalHeaderLabels(bpm_names) for row, bpm_name in enumerate(bpm_names): for col, axis in enumerate(("X", "Y")): pos = PyDMLabel() pos.channel = "ca://{}:{}".format(bpm_name, axis) pos.showUnits = True bpm_table.setCellWidget(row, col, pos) self.layout().addWidget(bpm_table)
class MCSDialog(QDialog): """A dialog to perform minimal cut set computation""" def __init__(self, appdata: CnaData, centralwidget): QDialog.__init__(self) self.setWindowTitle("Minimal Cut Sets Computation") self.appdata = appdata self.centralwidget = centralwidget self.eng = appdata.engine self.out = io.StringIO() self.err = io.StringIO() self.layout = QVBoxLayout() l1 = QLabel("Target Region(s)") self.layout.addWidget(l1) s1 = QHBoxLayout() completer = QCompleter( self.appdata.project.cobra_py_model.reactions.list_attr("id"), self) completer.setCaseSensitivity(Qt.CaseInsensitive) self.target_list = QTableWidget(1, 4) self.target_list.setHorizontalHeaderLabels( ["region no", "T", "≥/≤", "t"]) self.target_list.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.target_list.horizontalHeader().setSectionResizeMode(0, QHeaderView.Fixed) self.target_list.horizontalHeader().resizeSection(0, 100) self.target_list.horizontalHeader().setSectionResizeMode(2, QHeaderView.Fixed) self.target_list.horizontalHeader().resizeSection(2, 50) item = QLineEdit("1") self.target_list.setCellWidget(0, 0, item) item2 = QLineEdit("") item2.setCompleter(completer) self.target_list.setCellWidget(0, 1, item2) combo = QComboBox(self.target_list) combo.insertItem(1, "≤") combo.insertItem(2, "≥") self.target_list.setCellWidget(0, 2, combo) item = QLineEdit("0") self.target_list.setCellWidget(0, 3, item) s1.addWidget(self.target_list) s11 = QVBoxLayout() self.add_target = QPushButton("+") self.add_target.clicked.connect(self.add_target_region) self.rem_target = QPushButton("-") self.rem_target.clicked.connect(self.rem_target_region) s11.addWidget(self.add_target) s11.addWidget(self.rem_target) s1.addItem(s11) self.layout.addItem(s1) l2 = QLabel("Desired Region(s)") self.layout.addWidget(l2) s2 = QHBoxLayout() self.desired_list = QTableWidget(1, 4) self.desired_list.setHorizontalHeaderLabels( ["region no", "D", "≥/≤", "d"]) self.desired_list.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch) self.desired_list.horizontalHeader().setSectionResizeMode(0, QHeaderView.Fixed) self.desired_list.horizontalHeader().resizeSection(0, 100) self.desired_list.horizontalHeader().setSectionResizeMode(2, QHeaderView.Fixed) self.desired_list.horizontalHeader().resizeSection(2, 50) item = QLineEdit("1") self.desired_list.setCellWidget(0, 0, item) item2 = QLineEdit("") item2.setCompleter(completer) self.desired_list.setCellWidget(0, 1, item2) combo = QComboBox(self.desired_list) combo.insertItem(1, "≤") combo.insertItem(2, "≥") self.desired_list.setCellWidget(0, 2, combo) item = QLineEdit("0") self.desired_list.setCellWidget(0, 3, item) s2.addWidget(self.desired_list) s21 = QVBoxLayout() self.add_desire = QPushButton("+") self.add_desire.clicked.connect(self.add_desired_region) self.rem_desire = QPushButton("-") self.rem_desire.clicked.connect(self.rem_desired_region) s21.addWidget(self.add_desire) s21.addWidget(self.rem_desire) s2.addItem(s21) self.layout.addItem(s2) s3 = QHBoxLayout() sgx = QVBoxLayout() self.gen_kos = QCheckBox("Gene KOs") self.exclude_boundary = QCheckBox( "Exclude boundary\nreactions as cuts") sg1 = QHBoxLayout() s31 = QVBoxLayout() l = QLabel("Max. Solutions") s31.addWidget(l) l = QLabel("Max. Size") s31.addWidget(l) l = QLabel("Time Limit [sec]") s31.addWidget(l) sg1.addItem(s31) s32 = QVBoxLayout() self.max_solu = QLineEdit("inf") self.max_solu.setMaximumWidth(50) s32.addWidget(self.max_solu) self.max_size = QLineEdit("7") self.max_size.setMaximumWidth(50) s32.addWidget(self.max_size) self.time_limit = QLineEdit("inf") self.time_limit.setMaximumWidth(50) s32.addWidget(self.time_limit) sg1.addItem(s32) sgx.addWidget(self.gen_kos) sgx.addWidget(self.exclude_boundary) sgx.addItem(sg1) s3.addItem(sgx) g3 = QGroupBox("Solver") s33 = QVBoxLayout() self.bg1 = QButtonGroup() optlang_solver_name = interface_to_str( appdata.project.cobra_py_model.problem) self.solver_optlang = QRadioButton(f"{optlang_solver_name} (optlang)") self.solver_optlang.setToolTip( "Uses the solver specified by the current model.") s33.addWidget(self.solver_optlang) self.bg1.addButton(self.solver_optlang) self.solver_cplex_matlab = QRadioButton("CPLEX (MATLAB)") self.solver_cplex_matlab.setToolTip( "Only enabled with MATLAB and CPLEX") s33.addWidget(self.solver_cplex_matlab) self.bg1.addButton(self.solver_cplex_matlab) self.solver_cplex_java = QRadioButton("CPLEX (Octave)") self.solver_cplex_java.setToolTip("Only enabled with Octave and CPLEX") s33.addWidget(self.solver_cplex_java) self.bg1.addButton(self.solver_cplex_java) self.solver_intlinprog = QRadioButton("intlinprog (MATLAB)") self.solver_intlinprog.setToolTip("Only enabled with MATLAB") s33.addWidget(self.solver_intlinprog) self.bg1.addButton(self.solver_intlinprog) self.solver_glpk = QRadioButton("GLPK (Octave/MATLAB)") s33.addWidget(self.solver_glpk) self.bg1.addButton(self.solver_glpk) self.bg1.buttonClicked.connect(self.configure_solver_options) g3.setLayout(s33) s3.addWidget(g3) g4 = QGroupBox("MCS search") s34 = QVBoxLayout() self.bg2 = QButtonGroup() self.any_mcs = QRadioButton("any MCS (fast)") self.any_mcs.setChecked(True) s34.addWidget(self.any_mcs) self.bg2.addButton(self.any_mcs) # Search type: by cardinality only with CPLEX possible self.mcs_by_cardinality = QRadioButton("by cardinality") s34.addWidget(self.mcs_by_cardinality) self.bg2.addButton(self.mcs_by_cardinality) self.smalles_mcs_first = QRadioButton("smallest MCS first") s34.addWidget(self.smalles_mcs_first) self.bg2.addButton(self.smalles_mcs_first) g4.setLayout(s34) s3.addWidget(g4) self.layout.addItem(s3) # Disable incompatible combinations if appdata.selected_engine == 'None': self.solver_optlang.setChecked(True) self.solver_cplex_matlab.setEnabled(False) self.solver_cplex_java.setEnabled(False) self.solver_glpk.setEnabled(False) self.solver_intlinprog.setEnabled(False) if optlang_solver_name != 'cplex': self.mcs_by_cardinality.setEnabled(False) else: self.solver_glpk.setChecked(True) if not self.eng.is_cplex_matlab_ready(): self.solver_cplex_matlab.setEnabled(False) if not self.eng.is_cplex_java_ready(): self.solver_cplex_java.setEnabled(False) if self.appdata.is_matlab_set(): self.solver_cplex_java.setEnabled(False) if not self.appdata.is_matlab_set(): self.solver_cplex_matlab.setEnabled(False) self.solver_intlinprog.setEnabled(False) self.configure_solver_options() s4 = QVBoxLayout() self.consider_scenario = QCheckBox( "Consider constraint given by scenario") s4.addWidget(self.consider_scenario) self.advanced = QCheckBox( "Advanced: Define knockout/addition costs for genes/reactions") self.advanced.setEnabled(False) s4.addWidget(self.advanced) self.layout.addItem(s4) buttons = QHBoxLayout() # self.save = QPushButton("save") # buttons.addWidget(self.save) # self.load = QPushButton("load") # buttons.addWidget(self.load) self.compute_mcs = QPushButton("Compute MCS") buttons.addWidget(self.compute_mcs) # self.compute_mcs2 = QPushButton("Compute MCS2") # buttons.addWidget(self.compute_mcs2) self.cancel = QPushButton("Close") buttons.addWidget(self.cancel) self.layout.addItem(buttons) # max width for buttons self.add_target.setMaximumWidth(20) self.rem_target.setMaximumWidth(20) self.add_desire.setMaximumWidth(20) self.rem_desire.setMaximumWidth(20) self.setLayout(self.layout) # Connecting the signal self.cancel.clicked.connect(self.reject) self.compute_mcs.clicked.connect(self.compute) @Slot() def configure_solver_options(self): optlang_solver_name = interface_to_str( self.appdata.project.cobra_py_model.problem) if self.solver_optlang.isChecked(): self.gen_kos.setChecked(False) self.gen_kos.setEnabled(False) self.exclude_boundary.setEnabled(True) if optlang_solver_name != 'cplex': if self.mcs_by_cardinality.isChecked(): self.mcs_by_cardinality.setChecked(False) self.any_mcs.setChecked(True) self.mcs_by_cardinality.setEnabled(False) self.mcs_by_cardinality.setChecked(False) else: self.gen_kos.setEnabled(True) self.exclude_boundary.setChecked(False) self.exclude_boundary.setEnabled(False) self.mcs_by_cardinality.setEnabled(True) def add_target_region(self): i = self.target_list.rowCount() self.target_list.insertRow(i) completer = QCompleter( self.appdata.project.cobra_py_model.reactions.list_attr("id"), self) completer.setCaseSensitivity(Qt.CaseInsensitive) item = QLineEdit("1") self.target_list.setCellWidget(i, 0, item) item2 = QLineEdit("") item2.setCompleter(completer) self.target_list.setCellWidget(i, 1, item2) combo = QComboBox(self.target_list) combo.insertItem(1, "≤") combo.insertItem(2, "≥") self.target_list.setCellWidget(i, 2, combo) item = QLineEdit("0") self.target_list.setCellWidget(i, 3, item) def add_desired_region(self): i = self.desired_list.rowCount() self.desired_list.insertRow(i) completer = QCompleter( self.appdata.project.cobra_py_model.reactions.list_attr("id"), self) completer.setCaseSensitivity(Qt.CaseInsensitive) item = QLineEdit("1") self.desired_list.setCellWidget(i, 0, item) item2 = QLineEdit("") item2.setCompleter(completer) self.desired_list.setCellWidget(i, 1, item2) combo = QComboBox(self.desired_list) combo.insertItem(1, "≤") combo.insertItem(2, "≥") self.desired_list.setCellWidget(i, 2, combo) item = QLineEdit("0") self.desired_list.setCellWidget(i, 3, item) def rem_target_region(self): i = self.target_list.rowCount() self.target_list.removeRow(i-1) def rem_desired_region(self): i = self.desired_list.rowCount() self.desired_list.removeRow(i-1) def compute(self): if self.solver_optlang.isChecked(): self.compute_optlang() else: self.compute_legacy() def compute_legacy(self): self.setCursor(Qt.BusyCursor) # create CobraModel for matlab with self.appdata.project.cobra_py_model as model: if self.consider_scenario.isChecked(): # integrate scenario into model bounds for r in self.appdata.project.scen_values.keys(): model.reactions.get_by_id( r).bounds = self.appdata.project.scen_values[r] cobra.io.save_matlab_model(model, os.path.join( self.appdata.cna_path, "cobra_model.mat"), varname="cbmodel") self.eng.eval("load('cobra_model.mat')", nargout=0) try: self.eng.eval("cnap = CNAcobra2cna(cbmodel);", nargout=0, stdout=self.out, stderr=self.err) except Exception: output = io.StringIO() traceback.print_exc(file=output) exstr = output.getvalue() print(exstr) QMessageBox.warning(self, 'Unknown exception occured!', exstr+'\nPlease report the problem to:\n\ \nhttps://github.com/cnapy-org/CNApy/issues') return self.eng.eval("genes = [];", nargout=0, stdout=self.out, stderr=self.err) cmd = "maxSolutions = " + str(float(self.max_solu.text())) + ";" self.eng.eval(cmd, nargout=0, stdout=self.out, stderr=self.err) cmd = "maxSize = " + str(int(self.max_size.text())) + ";" self.eng.eval(cmd, nargout=0, stdout=self.out, stderr=self.err) cmd = "milp_time_limit = " + str(float(self.time_limit.text())) + ";" self.eng.eval(cmd, nargout=0, stdout=self.out, stderr=self.err) if self.gen_kos.isChecked(): self.eng.eval("gKOs = 1;", nargout=0) else: self.eng.eval("gKOs = 0;", nargout=0) if self.advanced.isChecked(): self.eng.eval("advanced_on = 1;", nargout=0) else: self.eng.eval("advanced_on = 0;", nargout=0) if self.solver_intlinprog.isChecked(): self.eng.eval("solver = 'intlinprog';", nargout=0) if self.solver_cplex_java.isChecked(): self.eng.eval("solver = 'java_cplex_new';", nargout=0) if self.solver_cplex_matlab.isChecked(): self.eng.eval("solver = 'matlab_cplex';", nargout=0) if self.solver_glpk.isChecked(): self.eng.eval("solver = 'glpk';", nargout=0) if self.any_mcs.isChecked(): self.eng.eval("mcs_search_mode = 'search_1';", nargout=0) elif self.mcs_by_cardinality.isChecked(): self.eng.eval("mcs_search_mode = 'search_2';", nargout=0) elif self.smalles_mcs_first.isChecked(): self.eng.eval("mcs_search_mode = 'search_3';", nargout=0) rows = self.target_list.rowCount() for i in range(0, rows): p1 = self.target_list.cellWidget(i, 0).text() p2 = self.target_list.cellWidget(i, 1).text() if self.target_list.cellWidget(i, 2).currentText() == '≤': p3 = "<=" else: p3 = ">=" p4 = self.target_list.cellWidget(i, 3).text() cmd = "dg_T = {[" + p1+"], '" + p2 + \ "', '" + p3 + "', [" + p4 + "']};" self.eng.eval(cmd, nargout=0, stdout=self.out, stderr=self.err) rows = self.desired_list.rowCount() for i in range(0, rows): p1 = self.desired_list.cellWidget(i, 0).text() p2 = self.desired_list.cellWidget(i, 1).text() if self.desired_list.cellWidget(i, 2).currentText() == '≤': p3 = "<=" else: p3 = ">=" p4 = self.desired_list.cellWidget(i, 3).text() cmd = "dg_D = {[" + p1+"], '" + p2 + \ "', '" + p3 + "', [" + p4 + "']};" self.eng.eval(cmd, nargout=0) # get some data self.eng.eval("reac_id = cellstr(cnap.reacID).';", nargout=0, stdout=self.out, stderr=self.err) mcs = [] values = [] reactions = [] reac_id = [] if self.appdata.is_matlab_set(): reac_id = self.eng.workspace['reac_id'] try: self.eng.eval("[mcs] = cnapy_compute_mcs(cnap, genes, maxSolutions, maxSize, milp_time_limit, gKOs, advanced_on, solver, mcs_search_mode, dg_T,dg_D);", nargout=0) except Exception: output = io.StringIO() traceback.print_exc(file=output) exstr = output.getvalue() print(exstr) QMessageBox.warning(self, 'Unknown exception occured!', exstr+'\nPlease report the problem to:\n\ \nhttps://github.com/cnapy-org/CNApy/issues') return else: self.eng.eval("[reaction, mcs, value] = find(mcs);", nargout=0, stdout=self.out, stderr=self.err) reactions = self.eng.workspace['reaction'] mcs = self.eng.workspace['mcs'] values = self.eng.workspace['value'] elif self.appdata.is_octave_ready(): reac_id = self.eng.pull('reac_id') reac_id = reac_id[0] try: self.eng.eval("[mcs] = cnapy_compute_mcs(cnap, genes, maxSolutions, maxSize, milp_time_limit, gKOs, advanced_on, solver, mcs_search_mode, dg_T,dg_D);", nargout=0) except Exception: output = io.StringIO() traceback.print_exc(file=output) exstr = output.getvalue() print(exstr) QMessageBox.warning(self, 'Unknown exception occured!', exstr+'\nPlease report the problem to:\n\ \nhttps://github.com/cnapy-org/CNApy/issues') return else: self.eng.eval("[reaction, mcs, value] = find(mcs);", nargout=0, stdout=self.out, stderr=self.err) reactions = self.eng.pull('reaction') mcs = self.eng.pull('mcs') values = self.eng.pull('value') if len(mcs) == 0: QMessageBox.information(self, 'No cut sets', 'Cut sets have not been calculated or do not exist.') else: last_mcs = 1 omcs = [] current_mcs = {} for i in range(0, len(reactions)): reacid = int(reactions[i][0]) reaction = reac_id[reacid-1] c_mcs = int(mcs[i][0]) c_value = int(values[i][0]) if c_value == -1: # -1 stands for removed which is 0 in the ui c_value = 0 if c_mcs > last_mcs: omcs.append(current_mcs) last_mcs = c_mcs current_mcs = {} current_mcs[reaction] = c_value omcs.append(current_mcs) self.appdata.project.modes = omcs self.centralwidget.mode_navigator.current = 0 QMessageBox.information(self, 'Cut sets found', str(len(omcs))+' Cut sets have been calculated.') self.centralwidget.update_mode() self.centralwidget.mode_navigator.title.setText("MCS Navigation") self.setCursor(Qt.ArrowCursor) def compute_optlang(self): self.setCursor(Qt.BusyCursor) max_mcs_num = float(self.max_solu.text()) max_mcs_size = int(self.max_size.text()) timeout = float(self.time_limit.text()) if timeout is float('inf'): timeout = None # if self.gen_kos.isChecked(): # self.eng.eval("gKOs = 1;", nargout=0) # else: # self.eng.eval("gKOs = 0;", nargout=0) # if self.advanced.isChecked(): # self.eng.eval("advanced_on = 1;", nargout=0) # else: # self.eng.eval("advanced_on = 0;", nargout=0) if self.smalles_mcs_first.isChecked(): enum_method = 1 elif self.mcs_by_cardinality.isChecked(): enum_method = 2 elif self.any_mcs.isChecked(): enum_method = 3 with self.appdata.project.cobra_py_model as model: if self.consider_scenario.isChecked(): # integrate scenario into model bounds for r in self.appdata.project.scen_values.keys(): model.reactions.get_by_id( r).bounds = self.appdata.project.scen_values[r] reac_id = model.reactions.list_attr("id") reac_id_symbols = cMCS_enumerator.get_reac_id_symbols(reac_id) rows = self.target_list.rowCount() targets = dict() for i in range(0, rows): p1 = self.target_list.cellWidget(i, 0).text() p2 = self.target_list.cellWidget(i, 1).text() if len(p1) > 0 and len(p2) > 0: if self.target_list.cellWidget(i, 2).currentText() == '≤': p3 = "<=" else: p3 = ">=" p4 = float(self.target_list.cellWidget(i, 3).text()) targets.setdefault(p1, []).append((p2, p3, p4)) targets = list(targets.values()) targets = [cMCS_enumerator.relations2leq_matrix(cMCS_enumerator.parse_relations( t, reac_id_symbols=reac_id_symbols), reac_id) for t in targets] rows = self.desired_list.rowCount() desired = dict() for i in range(0, rows): p1 = self.desired_list.cellWidget(i, 0).text() p2 = self.desired_list.cellWidget(i, 1).text() if len(p1) > 0 and len(p2) > 0: if self.desired_list.cellWidget(i, 2).currentText() == '≤': p3 = "<=" else: p3 = ">=" p4 = float(self.desired_list.cellWidget(i, 3).text()) desired.setdefault(p1, []).append((p2, p3, p4)) desired = list(desired.values()) desired = [cMCS_enumerator.relations2leq_matrix(cMCS_enumerator.parse_relations( d, reac_id_symbols=reac_id_symbols), reac_id) for d in desired] try: mcs = cMCS_enumerator.compute_mcs(model, targets=targets, desired=desired, enum_method=enum_method, max_mcs_size=max_mcs_size, max_mcs_num=max_mcs_num, timeout=timeout, exclude_boundary_reactions_as_cuts=self.exclude_boundary.isChecked()) except cMCS_enumerator.InfeasibleRegion as e: QMessageBox.warning(self, 'Cannot calculate MCS', str(e)) return targets, desired except Exception: output = io.StringIO() traceback.print_exc(file=output) exstr = output.getvalue() print(exstr) QMessageBox.warning(self, 'An exception has occured!', exstr+'\nPlease report the problem to:\n\ \nhttps://github.com/cnapy-org/CNApy/issues') return targets, desired finally: self.setCursor(Qt.ArrowCursor) if len(mcs) == 0: QMessageBox.information(self, 'No cut sets', 'Cut sets have not been calculated or do not exist.') return targets, desired omcs = [{reac_id[i]: 0 for i in m} for m in mcs] self.appdata.project.modes = omcs self.centralwidget.mode_navigator.current = 0 QMessageBox.information(self, 'Cut sets found', str(len(omcs))+' Cut sets have been calculated.') self.centralwidget.update_mode() self.centralwidget.mode_navigator.title.setText("MCS Navigation")
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 Extension2ReaderTable(QWidget): """Table showing extension to reader mappings with removal button. Widget presented in preferences-plugin dialog.""" valueChanged = Signal(int) def __init__(self, parent=None): super().__init__(parent=parent) self._table = QTableWidget() self._table.setShowGrid(False) self._populate_table() layout = QVBoxLayout() layout.addWidget(self._table) self.setLayout(layout) def _populate_table(self): """Add row for each extension to reader mapping in settings""" self._extension_col = 0 self._reader_col = 1 header_strs = [trans._('Extension'), trans._('Reader Plugin')] self._table.setColumnCount(2) self._table.setColumnWidth(self._extension_col, 100) self._table.setColumnWidth(self._reader_col, 150) self._table.verticalHeader().setVisible(False) self._table.setMinimumHeight(120) extension2reader = get_settings().plugins.extension2reader if len(extension2reader) > 0: self._table.setRowCount(len(extension2reader)) self._table.horizontalHeader().setStretchLastSection(True) self._table.horizontalHeader().setStyleSheet( 'border-bottom: 2px solid white;') self._table.setHorizontalHeaderLabels(header_strs) for row, (extension, plugin_name) in enumerate(extension2reader.items()): item = QTableWidgetItem(extension) item.setFlags(Qt.NoItemFlags) self._table.setItem(row, self._extension_col, item) plugin_widg = QWidget() # need object name to easily find row plugin_widg.setObjectName(f'{extension}') plugin_widg.setLayout(QHBoxLayout()) plugin_widg.layout().setContentsMargins(0, 0, 0, 0) plugin_label = QLabel(plugin_name) # need object name to easily work out which button was clicked remove_btn = QPushButton('x', objectName=f'{extension}') remove_btn.setFixedWidth(30) remove_btn.setStyleSheet('margin: 4px;') remove_btn.setToolTip( trans._('Remove this extension to reader association')) remove_btn.clicked.connect(self._remove_extension_assignment) plugin_widg.layout().addWidget(plugin_label) plugin_widg.layout().addWidget(remove_btn) self._table.setCellWidget(row, self._reader_col, plugin_widg) else: # Display that there are no extensions with reader associations self._table.setRowCount(1) self._table.setHorizontalHeaderLabels(header_strs) self._table.setColumnHidden(self._reader_col, True) self._table.setColumnWidth(self._extension_col, 200) item = QTableWidgetItem(trans._('No extensions found.')) item.setFlags(Qt.NoItemFlags) self._table.setItem(0, 0, item) def _remove_extension_assignment(self, event): """Delete extension to reader mapping setting and remove table row""" extension_to_remove = self.sender().objectName() current_settings = get_settings().plugins.extension2reader # need explicit assignment to new object here for persistence get_settings().plugins.extension2reader = { k: v for k, v in current_settings.items() if k != extension_to_remove } for i in range(self._table.rowCount()): row_widg_name = self._table.cellWidget( i, self._reader_col).objectName() if row_widg_name == extension_to_remove: self._table.removeRow(i) return
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 RgbSelectionWidget(QWidget): signal_update_map_selections = Signal() def __init__(self): super().__init__() self._range_table = [] self._limit_table = [] self._rgb_keys = ["red", "green", "blue"] self._rgb_dict = {_: None for _ in self._rgb_keys} widget_layout = self._setup_rgb_widget() self.setLayout(widget_layout) sp = QSizePolicy() sp.setControlType(QSizePolicy.PushButton) sp.setHorizontalPolicy(QSizePolicy.Expanding) sp.setVerticalPolicy(QSizePolicy.Fixed) self.setSizePolicy(sp) def _setup_rgb_element(self, n_row, *, rb_check=0): """ Parameters ---------- rb_check: int The number of QRadioButton to check. Typically this would be the row number. """ combo_elements = ComboBoxNamed(name=f"{n_row}") # combo_elements.setSizeAdjustPolicy(QComboBox.AdjustToContents) # Set text color for QComboBox widget (necessary if the program is used with Dark theme) pal = combo_elements.palette() pal.setColor(QPalette.ButtonText, Qt.black) combo_elements.setPalette(pal) # Set text color for drop-down view (necessary if the program is used with Dark theme) pal = combo_elements.view().palette() pal.setColor(QPalette.Text, Qt.black) combo_elements.view().setPalette(pal) btns = [QRadioButton(), QRadioButton(), QRadioButton()] if 0 <= rb_check < len(btns): btns[rb_check].setChecked(True) # Color is set for operation with Dark theme for btn in btns: pal = btn.palette() pal.setColor(QPalette.Text, Qt.black) btn.setPalette(pal) btn_group = QButtonGroup() for btn in btns: btn_group.addButton(btn) rng = RangeManager(name=f"{n_row}", add_sliders=True) rng.setTextColor([0, 0, 0]) # Set text color to 'black' # Set some text in edit boxes (just to demonstrate how the controls will look like) rng.le_min_value.setText("0.0") rng.le_max_value.setText("1.0") rng.setAlignment(Qt.AlignCenter) return combo_elements, btns, rng, btn_group def _enable_selection_events(self, enable): if enable: if not self.elements_btn_groups_events_enabled: for btn_group in self.elements_btn_groups: btn_group.buttonToggled.connect(self.rb_toggled) for el_combo in self.elements_combo: el_combo.currentIndexChanged.connect( self.combo_element_current_index_changed) for el_range in self.elements_range: el_range.selection_changed.connect( self.range_selection_changed) self.elements_btn_groups_events_enabled = True else: if self.elements_btn_groups_events_enabled: for btn_group in self.elements_btn_groups: btn_group.buttonToggled.disconnect(self.rb_toggled) for el_combo in self.elements_combo: el_combo.currentIndexChanged.disconnect( self.combo_element_current_index_changed) # Disconnecting the Range Manager signals is not necessary, but let's do it for consistency for el_range in self.elements_range: el_range.selection_changed.disconnect( self.range_selection_changed) self.elements_btn_groups_events_enabled = False def _setup_rgb_widget(self): self.elements_combo = [] self.elements_rb_color = [] self.elements_range = [] self.elements_btn_groups = [] self.elements_btn_groups_events_enabled = False self.row_colors = [] self.table = QTableWidget() # Horizontal header entries tbl_labels = ["Element", "Red", "Green", "Blue", "Range"] # The list of columns that stretch with the table self.tbl_cols_stretch = ("Range", ) self.table.setColumnCount(len(tbl_labels)) self.table.setRowCount(3) self.table.setHorizontalHeaderLabels(tbl_labels) self.table.verticalHeader().hide() self.table.setSelectionMode(QTableWidget.NoSelection) header = self.table.horizontalHeader() for n, lbl in enumerate(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) vheader = self.table.verticalHeader() vheader.setSectionResizeMode(QHeaderView.Stretch) # ResizeToContents) for n_row in range(3): combo_elements, btns, rng, btn_group = self._setup_rgb_element( n_row, rb_check=n_row) combo_elements.setMinimumWidth(180) self.table.setCellWidget(n_row, 0, combo_elements) for i, btn in enumerate(btns): item = QWidget() item_hbox = QHBoxLayout(item) item_hbox.addWidget(btn) item_hbox.setAlignment(Qt.AlignCenter) item_hbox.setContentsMargins(0, 0, 0, 0) item.setMinimumWidth(70) self.table.setCellWidget(n_row, i + 1, item) rng.setMinimumWidth(200) rng.setMaximumWidth(400) self.table.setCellWidget(n_row, 4, rng) self.elements_combo.append(combo_elements) self.elements_rb_color.append(btns) self.elements_range.append(rng) self.elements_btn_groups.append(btn_group) self.row_colors.append(self._rgb_keys[n_row]) # Colors that are used to paint rows of the table in RGB colors br = 150 self._rgb_row_colors = { "red": (255, br, br), "green": (br, 255, br), "blue": (br, br, 255) } self._rgb_color_keys = ["red", "green", "blue"] # Set initial colors for n_row in range(self.table.rowCount()): self._set_row_color(n_row) self._enable_selection_events(True) self.table.resizeRowsToContents() # Table height is computed based on content. It doesn't seem # to account for the height of custom widgets, but the table # looks good enough table_height = 0 for n_row in range(self.table.rowCount()): table_height += self.table.rowHeight(n_row) self.table.setMaximumHeight(table_height) table_width = 650 self.table.setMinimumWidth(table_width) self.table.setMaximumWidth(800) hbox = QHBoxLayout() hbox.addWidget(self.table) return hbox def combo_element_current_index_changed(self, name, index): if index < 0 or index >= len(self._range_table): return n_row = int(name) sel_eline = self._range_table[index][0] row_color = self.row_colors[n_row] self._rgb_dict[row_color] = sel_eline self.elements_range[n_row].set_range(self._range_table[index][1], self._range_table[index][2]) self.elements_range[n_row].set_selection( value_low=self._limit_table[index][1], value_high=self._limit_table[index][2]) self._update_map_selections() def range_selection_changed(self, v_low, v_high, name): n_row = int(name) row_color = self.row_colors[n_row] sel_eline = self._rgb_dict[row_color] ind = None try: ind = [_[0] for _ in self._limit_table].index(sel_eline) except ValueError: pass if ind is not None: self._limit_table[ind][1] = v_low self._limit_table[ind][2] = v_high self._update_map_selections() # We are not preventing users to select the same emission line in to rows. # Update the selected limits in other rows where the same element is selected. for nr, el_range in enumerate(self.elements_range): if (nr != n_row) and (self._rgb_dict[self.row_colors[nr]] == sel_eline): el_range.set_selection(value_low=v_low, value_high=v_high) def _get_selected_row_color(self, n_row): color_key = None btns = self.elements_rb_color[n_row] for n, btn in enumerate(btns): if btn.isChecked(): color_key = self._rgb_color_keys[n] break return color_key def _set_row_color(self, n_row, *, color_key=None): """ Parameters ---------- n_row: int The row number that needs background color change (0..2 if table has 3 rows) color_key: int Color key: "red", "green" or "blue" """ if color_key is None: color_key = self._get_selected_row_color(n_row) if color_key is None: return self.row_colors[n_row] = color_key rgb = self._rgb_row_colors[color_key] # The following code is based on the arrangement of the widgets in the table # Modify the code if widgets are arranged differently or the table structure # is changed for n_col in range(self.table.columnCount()): wd = self.table.cellWidget(n_row, n_col) if n_col == 0: # Combo box: update both QComboBox and QWidget backgrounds # QWidget - background of the drop-down selection list css1 = get_background_css(rgb, widget="QComboBox", editable=False) css2 = get_background_css(rgb, widget="QWidget", editable=True) wd.setStyleSheet(css2 + css1) elif n_col <= 3: # 3 QRadioButton's. The buttons are inserted into QWidget objects, # and we need to change backgrounds of QWidgets, not only buttons. wd.setStyleSheet( get_background_css(rgb, widget="QWidget", editable=False)) elif n_col == 4: # Custom RangeManager widget, color is updated using custom method wd.setBackground(rgb) n_col = self._rgb_color_keys.index(color_key) for n, n_btn in enumerate(self.elements_rb_color[n_row]): check_status = True if n == n_col else False n_btn.setChecked(check_status) def _fill_table(self): self._enable_selection_events(False) eline_list = [_[0] for _ in self._range_table] for n_row in range(self.table.rowCount()): self.elements_combo[n_row].clear() self.elements_combo[n_row].addItems(eline_list) for n_row, color in enumerate(self._rgb_color_keys): # Initially set colors in order self._set_row_color(n_row, color_key=color) eline_key = self._rgb_dict[color] if eline_key is not None: try: ind = eline_list.index(eline_key) self.elements_combo[n_row].setCurrentIndex(ind) range_low, range_high = self._range_table[ind][1:] self.elements_range[n_row].set_range(range_low, range_high) sel_low, sel_high = self._limit_table[ind][1:] self.elements_range[n_row].set_selection( value_low=sel_low, value_high=sel_high) except ValueError: pass else: self.elements_combo[n_row].setCurrentIndex(-1) # Deselect all self.elements_range[n_row].set_range(0, 1) self.elements_range[n_row].set_selection(value_low=0, value_high=1) self._enable_selection_events(True) def _find_rbutton(self, button): for nr, btns in enumerate(self.elements_rb_color): for nc, btn in enumerate(btns): if btn == button: # Return tuple (nr, nc) return nr, nc # Return None if the button is not found (this shouldn't happen) return None def rb_toggled(self, button, state): if state: # Ignore signals from unchecked buttons nr, nc = self._find_rbutton(button) color_current = self.row_colors[nr] color_to_set = self._rgb_color_keys[nc] nr_switch = self.row_colors.index(color_to_set) self._enable_selection_events(False) self._set_row_color(nr, color_key=color_to_set) self._set_row_color(nr_switch, color_key=color_current) # Swap selected maps tmp = self._rgb_dict[color_to_set] self._rgb_dict[color_to_set] = self._rgb_dict[color_current] self._rgb_dict[color_current] = tmp self._enable_selection_events(True) self._update_map_selections() def set_ranges_and_limits(self, *, range_table=None, limit_table=None, rgb_dict=None): if range_table is not None: self._range_table = copy.deepcopy(range_table) if limit_table is not None: self._limit_table = copy.deepcopy(limit_table) if rgb_dict is not None: self._rgb_dict = rgb_dict.copy() self._fill_table() def _update_map_selections(self): """Upload the selections (limit table) and update plot""" self.signal_update_map_selections.emit()
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 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()
def switch_rows(table: QTableWidget, old_position, new_position): """ Helper function to switch a row in a table. Works for booking and entry table. :param table: :param old_position: :param new_position: :return: """ for col_index in range(table.columnCount()): old_item = table.item(old_position, col_index) new_item = table.item(new_position, col_index) if old_item is not None and new_item is not None: old_text = old_item.text() new_text = new_item.text() table.setItem(old_position, col_index, QTableWidgetItem(new_text)) table.setItem(new_position, col_index, QTableWidgetItem(old_text)) else: old_cell_widget = table.cellWidget(old_position, col_index) new_cell_widget = table.cellWidget(new_position, col_index) if old_cell_widget is not None and new_cell_widget is not None: if isinstance(old_cell_widget, QTimeEdit) and isinstance(new_cell_widget, QTimeEdit): qte = QTimeEdit(table) qte.setTime(new_cell_widget.time()) table.setCellWidget(old_position, col_index, qte) qte = QTimeEdit(table) qte.setTime(old_cell_widget.time()) table.setCellWidget(new_position, col_index, qte) if isinstance(old_cell_widget, QDoubleSpinBox) and isinstance(new_cell_widget, QDoubleSpinBox): qdsb = QDoubleSpinBox(table) qdsb.setValue(new_cell_widget.value()) table.setCellWidget(old_position, col_index, qdsb) qdsb = QDoubleSpinBox(table) qdsb.setValue(old_cell_widget.value()) table.setCellWidget(new_position, col_index, qdsb) if isinstance(old_cell_widget, QCheckBox) and isinstance(new_cell_widget, QCheckBox): qcb = QCheckBox(table) qcb.setChecked(new_cell_widget.isChecked()) table.setCellWidget(old_position, col_index, qcb) qcb = QCheckBox(table) qcb.setChecked(old_cell_widget.isChecked()) table.setCellWidget(new_position, col_index, qcb)
class Extension2ReaderTable(QWidget): """Table showing extension to reader mappings with removal button. Widget presented in preferences-plugin dialog.""" valueChanged = Signal(int) def __init__(self, parent=None, npe2_readers=None, npe1_readers=None): super().__init__(parent=parent) npe2, npe1 = get_all_readers() if npe2_readers is None: npe2_readers = npe2 if npe1_readers is None: npe1_readers = npe1 self._npe2_readers = npe2_readers self._npe1_readers = npe1_readers self._table = QTableWidget() self._table.setShowGrid(False) self._set_up_table() self._edit_row = self._make_new_preference_row() self._populate_table() instructions = QLabel( trans. _('Enter a filename pattern to associate with a reader e.g. "*.tif" for all TIFF files.' ) + trans. _('Available readers will be filtered to those compatible with your pattern. Hover over a reader to see what patterns it accepts.' ) + trans. _('\n\nPreference saving for folder readers is not supported, so these readers are not shown.' ) + trans. _('\n\nFor documentation on valid filename patterns, see https://docs.python.org/3/library/fnmatch.html' )) instructions.setWordWrap(True) instructions.setOpenExternalLinks(True) layout = QVBoxLayout() instructions.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Expanding) layout.addWidget(instructions) layout.addWidget(self._edit_row) layout.addWidget(self._table) self.setLayout(layout) def _set_up_table(self): """Add table columns and headers, define styling""" self._fn_pattern_col = 0 self._reader_col = 1 header_strs = [trans._('Filename Pattern'), trans._('Reader Plugin')] self._table.setColumnCount(2) self._table.setColumnWidth(self._fn_pattern_col, 200) self._table.setColumnWidth(self._reader_col, 200) self._table.verticalHeader().setVisible(False) self._table.setMinimumHeight(120) self._table.horizontalHeader().setStyleSheet( 'border-bottom: 2px solid white;') self._table.setHorizontalHeaderLabels(header_strs) self._table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) def _populate_table(self): """Add row for each extension to reader mapping in settings""" fnpattern2reader = get_settings().plugins.extension2reader if len(fnpattern2reader) > 0: for fn_pattern, plugin_name in fnpattern2reader.items(): self._add_new_row(fn_pattern, plugin_name) else: # Display that there are no filename patterns with reader associations self._display_no_preferences_found() def _make_new_preference_row(self): """Make row for user to add a new filename pattern assignment""" edit_row_widget = QWidget() edit_row_widget.setLayout(QGridLayout()) edit_row_widget.layout().setContentsMargins(0, 0, 0, 0) self._fn_pattern_edit = QLineEdit() self._fn_pattern_edit.setPlaceholderText( "Start typing filename pattern...") self._fn_pattern_edit.textChanged.connect( self._filter_compatible_readers) add_reader_widg = QWidget() add_reader_widg.setLayout(QHBoxLayout()) add_reader_widg.layout().setContentsMargins(0, 0, 0, 0) self._new_reader_dropdown = QComboBox() for i, (plugin_name, display_name) in enumerate( sorted(dict(self._npe2_readers, **self._npe1_readers).items())): self._add_reader_choice(i, plugin_name, display_name) add_btn = QPushButton('Add') add_btn.setToolTip(trans._('Save reader preference for pattern')) add_btn.clicked.connect(self._save_new_preference) add_reader_widg.layout().addWidget(self._new_reader_dropdown) add_reader_widg.layout().addWidget(add_btn) edit_row_widget.layout().addWidget( self._fn_pattern_edit, 0, 0, ) edit_row_widget.layout().addWidget(add_reader_widg, 0, 1) return edit_row_widget def _display_no_preferences_found(self): self._table.setRowCount(1) item = QTableWidgetItem(trans._('No filename preferences found.')) item.setFlags(Qt.NoItemFlags) self._table.setItem(self._fn_pattern_col, 0, item) def _add_reader_choice(self, i, plugin_name, display_name): """Add dropdown item for plugin_name with reader pattern tooltip""" reader_patterns = get_filename_patterns_for_reader(plugin_name) # TODO: no reader_patterns means directory reader, # we don't support preference association yet if not reader_patterns: return self._new_reader_dropdown.addItem(display_name, plugin_name) if '*' in reader_patterns: tooltip_text = 'Accepts all' else: reader_patterns_formatted = ', '.join(sorted( list(reader_patterns))) tooltip_text = f'Accepts: {reader_patterns_formatted}' self._new_reader_dropdown.setItemData(i, tooltip_text, role=Qt.ToolTipRole) def _filter_compatible_readers(self, new_pattern): """Filter reader dropwdown items to those that accept `new_extension`""" self._new_reader_dropdown.clear() readers = self._npe2_readers.copy() to_delete = [] compatible_readers = get_potential_readers(new_pattern) for plugin_name, display_name in readers.items(): if plugin_name not in compatible_readers: to_delete.append(plugin_name) for reader in to_delete: del readers[reader] readers.update(self._npe1_readers) if not readers: self._new_reader_dropdown.addItem("None available") else: for i, (plugin_name, display_name) in enumerate(sorted(readers.items())): self._add_reader_choice(i, plugin_name, display_name) def _save_new_preference(self, event): """Save current preference to settings and show in table""" fn_pattern = self._fn_pattern_edit.text() reader = self._new_reader_dropdown.currentData() if not fn_pattern or not reader: return # if user types pattern that starts with a . it's probably a file extension so prepend the * if fn_pattern.startswith('.'): fn_pattern = f'*{fn_pattern}' if fn_pattern in get_settings().plugins.extension2reader: self._edit_existing_preference(fn_pattern, reader) else: self._add_new_row(fn_pattern, reader) get_settings().plugins.extension2reader = { **get_settings().plugins.extension2reader, fn_pattern: reader, } def _edit_existing_preference(self, fn_pattern, reader): """Edit existing extension preference""" current_reader_label = self.findChild(QLabel, fn_pattern) if reader in self._npe2_readers: reader = self._npe2_readers[reader] current_reader_label.setText(reader) def _add_new_row(self, fn_pattern, reader): """Add new reader preference to table""" last_row = self._table.rowCount() if (last_row == 1 and 'No filename preferences found' in self._table.item(0, 0).text()): self._table.removeRow(0) last_row = 0 self._table.insertRow(last_row) item = QTableWidgetItem(fn_pattern) item.setFlags(Qt.NoItemFlags) self._table.setItem(last_row, self._fn_pattern_col, item) plugin_widg = QWidget() # need object name to easily find row plugin_widg.setObjectName(f'{fn_pattern}') plugin_widg.setLayout(QHBoxLayout()) plugin_widg.layout().setContentsMargins(0, 0, 0, 0) if reader in self._npe2_readers: reader = self._npe2_readers[reader] plugin_label = QLabel(reader, objectName=fn_pattern) # need object name to easily work out which button was clicked remove_btn = QPushButton('X', objectName=fn_pattern) remove_btn.setFixedWidth(30) remove_btn.setStyleSheet('margin: 4px;') remove_btn.setToolTip( trans._('Remove this filename pattern to reader association')) remove_btn.clicked.connect(self.remove_existing_preference) plugin_widg.layout().addWidget(plugin_label) plugin_widg.layout().addWidget(remove_btn) self._table.setCellWidget(last_row, self._reader_col, plugin_widg) def remove_existing_preference(self, event): """Delete extension to reader mapping setting and remove table row""" pattern_to_remove = self.sender().objectName() current_settings = get_settings().plugins.extension2reader # need explicit assignment to new object here for persistence get_settings().plugins.extension2reader = { k: v for k, v in current_settings.items() if k != pattern_to_remove } for i in range(self._table.rowCount()): row_widg_name = self._table.cellWidget( i, self._reader_col).objectName() if row_widg_name == pattern_to_remove: self._table.removeRow(i) break if self._table.rowCount() == 0: self._display_no_preferences_found() def value(self): """Return extension:reader mapping from settings. Returns ------- Dict[str, str] mapping of extension to reader plugin display name """ return get_settings().plugins.extension2reader
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()