class CheckHLayout(QHBoxLayout): """Check boxes hlayout with QButtonGroup""" def __init__(self, boxes, checks, parent=None): QHBoxLayout.__init__(self) self.setSpacing(0) self.group = QButtonGroup() self.group.setExclusive(False) for i, (box, check) in enumerate(zip(boxes, checks)): cbx = QCheckBox(box) cbx.setChecked(eval(check)) self.addWidget(cbx) self.group.addButton(cbx, i) def values(self): return [cbx.isChecked() for cbx in self.group.buttons()] def setStyleSheet(self, style): for cbx in self.group.buttons(): cbx.setStyleSheet(style)
class RadioLayout(QVBoxLayout): """Radio buttons layout with QButtonGroup""" def __init__(self, buttons, index, parent=None): QVBoxLayout.__init__(self) self.setSpacing(0) self.group = QButtonGroup() for i, button in enumerate(buttons): btn = QRadioButton(button) if i == index: btn.setChecked(True) self.addWidget(btn) self.group.addButton(btn, i) def currentIndex(self): return self.group.checkedId() def setStyleSheet(self, style): for btn in self.group.buttons(): btn.setStyleSheet(style)
class MultiCheckWidget(QGroupBox): """Qt Widget to show multiple checkboxes for a sequence of numbers. Args: count: The number of checkboxes to show. title: Display title for group of checkboxes. selected: List of checkbox numbers to initially check. default: Whether to default boxes as checked. """ def __init__(self, *args, count: int, title: Optional[str] = "", selected: Optional[List] = None, default: Optional[bool] = False, **kwargs): super(MultiCheckWidget, self).__init__(*args, **kwargs) # QButtonGroup is the logical container # it allows us to get list of checked boxes more easily self.check_group = QButtonGroup() self.check_group.setExclusive(False) # more than one can be checked if title != "": self.setTitle(title) self.setFlat(False) else: self.setFlat(True) if selected is None: selected = list(range(count)) if default else [] check_layout = QGridLayout() self.setLayout(check_layout) for i in range(count): check = QCheckBox("%d" % (i)) # call signal/slot on self when one of the checkboxes is changed check.stateChanged.connect(lambda e: self.selectionChanged.emit()) self.check_group.addButton(check, i) check_layout.addWidget(check, i // 8, i % 8) self.setSelected(selected) """ selectionChanged signal sent when a checkbox gets a stateChanged signal """ selectionChanged = Signal() def getSelected(self) -> list: """Method to get list of the checked checkboxes. Returns: list of checked checkboxes """ selected = [] for check_button in self.check_group.buttons(): if check_button.isChecked(): selected.append(self.check_group.id(check_button)) return selected def setSelected(self, selected: list): """Method to set some checkboxes as checked. Args: selected: List of checkboxes to check. Returns: None """ for check_button in self.check_group.buttons(): if self.check_group.id(check_button) in selected: check_button.setChecked(True) else: check_button.setChecked(False) def boundingRect(self) -> QRectF: """Method required by Qt. """ return QRectF() def paint(self, painter, option, widget=None): """Method required by Qt. """ pass
class ParameterTagToolBar(QToolBar): """A toolbar to add items using drag and drop actions.""" tag_button_toggled = Signal("QVariant", "bool") manage_tags_action_triggered = Signal("bool") tag_actions_added = Signal("QVariant", "QVariant") def __init__(self, parent, db_mngr, *db_maps): """ Args: parent (DataStoreForm): tree or graph view form db_mngr (SpineDBManager): the DB manager for interacting with the db db_maps (iter): DiffDatabaseMapping instances """ super().__init__("Parameter Tag Toolbar", parent=parent) self.db_mngr = db_mngr self.db_maps = db_maps label = QLabel("Parameter tag") self.addWidget(label) self.tag_button_group = QButtonGroup(self) self.tag_button_group.setExclusive(False) self.actions = [] self.db_map_ids = [] empty = QWidget() empty.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.empty_action = self.addWidget(empty) button = QPushButton("Manage tags...") self.addWidget(button) # noinspection PyUnresolvedReferences # pylint: disable=unnecessary-lambda button.clicked.connect(lambda checked: self.manage_tags_action_triggered.emit(checked)) self.setStyleSheet(PARAMETER_TAG_TOOLBAR_SS) self.setObjectName("ParameterTagToolbar") self.tag_actions_added.connect(self._add_db_map_tag_actions) def init_toolbar(self): for button in self.tag_button_group.buttons(): self.tag_button_group.removeButton(button) for action in self.actions: self.removeAction(action) action = QAction("untagged") self.insertAction(self.empty_action, action) action.setCheckable(True) button = self.widgetForAction(action) self.tag_button_group.addButton(button, id=0) self.actions = [action] self.db_map_ids = [[(db_map, 0) for db_map in self.db_maps]] tag_data = {} for db_map in self.db_maps: for parameter_tag in self.db_mngr.get_items(db_map, "parameter tag"): tag_data.setdefault(parameter_tag["tag"], {})[db_map] = parameter_tag["id"] for tag, db_map_data in tag_data.items(): action = QAction(tag) self.insertAction(self.empty_action, action) action.setCheckable(True) button = self.widgetForAction(action) self.tag_button_group.addButton(button, id=len(self.db_map_ids)) self.actions.append(action) self.db_map_ids.append(list(db_map_data.items())) self.tag_button_group.buttonToggled["int", "bool"].connect( lambda i, checked: self.tag_button_toggled.emit(self.db_map_ids[i], checked) ) def receive_parameter_tags_added(self, db_map_data): for db_map, parameter_tags in db_map_data.items(): self.tag_actions_added.emit(db_map, parameter_tags) @Slot("QVariant", "QVariant") def _add_db_map_tag_actions(self, db_map, parameter_tags): action_texts = [a.text() for a in self.actions] for parameter_tag in parameter_tags: if parameter_tag["tag"] in action_texts: # Already a tag named after that, add db_map id information i = action_texts.index(parameter_tag["tag"]) self.db_map_ids[i].append((db_map, parameter_tag["id"])) else: action = QAction(parameter_tag["tag"]) self.insertAction(self.empty_action, action) action.setCheckable(True) button = self.widgetForAction(action) self.tag_button_group.addButton(button, id=len(self.db_map_ids)) self.actions.append(action) self.db_map_ids.append([(db_map, parameter_tag["id"])]) action_texts.append(action.text()) def receive_parameter_tags_removed(self, db_map_data): for db_map, parameter_tags in db_map_data.items(): parameter_tag_ids = {x["id"] for x in parameter_tags} self._remove_db_map_tag_actions(db_map, parameter_tag_ids) def _remove_db_map_tag_actions(self, db_map, parameter_tag_ids): for tag_id in parameter_tag_ids: i = next(k for k, x in enumerate(self.db_map_ids) if (db_map, tag_id) in x) self.db_map_ids[i].remove((db_map, tag_id)) if not self.db_map_ids[i]: self.db_map_ids.pop(i) self.removeAction(self.actions.pop(i)) def receive_parameter_tags_updated(self, db_map_data): for db_map, parameter_tags in db_map_data.items(): self._update_db_map_tag_actions(db_map, parameter_tags) def _update_db_map_tag_actions(self, db_map, parameter_tags): for parameter_tag in parameter_tags: i = next(k for k, x in enumerate(self.db_map_ids) if (db_map, parameter_tag["id"]) in x) action = self.actions[i] action.setText(parameter_tag["tag"])
class SCOUTS(QMainWindow): """Main Window Widget for SCOUTS.""" style = { 'title': 'QLabel {font-size: 18pt; font-weight: 600}', 'header': 'QLabel {font-size: 12pt; font-weight: 520}', 'label': 'QLabel {font-size: 10pt}', 'button': 'QPushButton {font-size: 10pt}', 'md button': 'QPushButton {font-size: 12pt}', 'run button': 'QPushButton {font-size: 18pt; font-weight: 600}', 'line edit': 'QLineEdit {font-size: 10pt}', 'checkbox': 'QCheckBox {font-size: 10pt}', 'radio button': 'QRadioButton {font-size: 10pt}' } def __init__(self) -> None: """SCOUTS Constructor. Defines all aspects of the GUI.""" # ### # ### Main Window setup # ### # Inherits from QMainWindow super().__init__() self.rootdir = get_project_root() self.threadpool = QThreadPool() # Sets values for QMainWindow self.setWindowTitle("SCOUTS") self.setWindowIcon( QIcon( os.path.abspath(os.path.join(self.rootdir, 'src', 'scouts.ico')))) # Creates StackedWidget as QMainWindow's central widget self.stacked_pages = QStackedWidget(self) self.setCentralWidget(self.stacked_pages) # Creates Widgets for individual "pages" and adds them to the StackedWidget self.main_page = QWidget() self.samples_page = QWidget() self.gating_page = QWidget() self.pages = (self.main_page, self.samples_page, self.gating_page) for page in self.pages: self.stacked_pages.addWidget(page) # ## Sets widget at program startup self.stacked_pages.setCurrentWidget(self.main_page) # ### # ### MAIN PAGE # ### # Main page layout self.main_layout = QVBoxLayout(self.main_page) # Title section # Title self.title = QLabel(self.main_page) self.title.setText('SCOUTS - Single Cell Outlier Selector') self.title.setStyleSheet(self.style['title']) self.title.adjustSize() self.main_layout.addWidget(self.title) # ## Input section # Input header self.input_header = QLabel(self.main_page) self.input_header.setText('Input settings') self.input_header.setStyleSheet(self.style['header']) self.main_layout.addChildWidget(self.input_header) self.input_header.adjustSize() self.main_layout.addWidget(self.input_header) # Input frame self.input_frame = QFrame(self.main_page) self.input_frame.setFrameShape(QFrame.StyledPanel) self.input_frame.setLayout(QFormLayout()) self.main_layout.addWidget(self.input_frame) # Input button self.input_button = QPushButton(self.main_page) self.input_button.setStyleSheet(self.style['button']) self.set_icon(self.input_button, 'x-office-spreadsheet') self.input_button.setObjectName('input') self.input_button.setText(' Select input file (.xlsx or .csv)') self.input_button.clicked.connect(self.get_path) # Input path box self.input_path = QLineEdit(self.main_page) self.input_path.setObjectName('input_path') self.input_path.setStyleSheet(self.style['line edit']) # Go to sample naming page self.samples_button = QPushButton(self.main_page) self.samples_button.setStyleSheet(self.style['button']) self.set_icon(self.samples_button, 'preferences-other') self.samples_button.setText(' Name samples...') self.samples_button.clicked.connect(self.goto_samples_page) # Go to gating page self.gates_button = QPushButton(self.main_page) self.gates_button.setStyleSheet(self.style['button']) self.set_icon(self.gates_button, 'preferences-other') self.gates_button.setText(' Gating && outlier options...') self.gates_button.clicked.connect(self.goto_gates_page) # Add widgets above to input frame Layout self.input_frame.layout().addRow(self.input_button, self.input_path) self.input_frame.layout().addRow(self.samples_button) self.input_frame.layout().addRow(self.gates_button) # ## Analysis section # Analysis header self.analysis_header = QLabel(self.main_page) self.analysis_header.setText('Analysis settings') self.analysis_header.setStyleSheet(self.style['header']) self.analysis_header.adjustSize() self.main_layout.addWidget(self.analysis_header) # Analysis frame self.analysis_frame = QFrame(self.main_page) self.analysis_frame.setFrameShape(QFrame.StyledPanel) self.analysis_frame.setLayout(QVBoxLayout()) self.main_layout.addWidget(self.analysis_frame) # Cutoff text self.cutoff_text = QLabel(self.main_page) self.cutoff_text.setText('Type of outlier to select:') self.cutoff_text.setToolTip( 'Choose whether to select outliers using the cutoff value from a reference\n' 'sample (OutR) or by using the cutoff value calculated for each sample\n' 'individually (OutS)') self.cutoff_text.setStyleSheet(self.style['label']) # Cutoff button group self.cutoff_group = QButtonGroup(self) # Cutoff by sample self.cutoff_sample = QRadioButton(self.main_page) self.cutoff_sample.setText('OutS') self.cutoff_sample.setObjectName('sample') self.cutoff_sample.setStyleSheet(self.style['radio button']) self.cutoff_sample.setChecked(True) self.cutoff_group.addButton(self.cutoff_sample) # Cutoff by reference self.cutoff_reference = QRadioButton(self.main_page) self.cutoff_reference.setText('OutR') self.cutoff_reference.setObjectName('ref') self.cutoff_reference.setStyleSheet(self.style['radio button']) self.cutoff_group.addButton(self.cutoff_reference) # Both cutoffs self.cutoff_both = QRadioButton(self.main_page) self.cutoff_both.setText('both') self.cutoff_both.setObjectName('sample ref') self.cutoff_both.setStyleSheet(self.style['radio button']) self.cutoff_group.addButton(self.cutoff_both) # Markers text self.markers_text = QLabel(self.main_page) self.markers_text.setStyleSheet(self.style['label']) self.markers_text.setText('Show results for:') self.markers_text.setToolTip( 'Individual markers: for each marker, select outliers\n' 'Any marker: select cells that are outliers for AT LEAST one marker' ) # Markers button group self.markers_group = QButtonGroup(self) # Single marker self.single_marker = QRadioButton(self.main_page) self.single_marker.setText('individual markers') self.single_marker.setObjectName('single') self.single_marker.setStyleSheet(self.style['radio button']) self.single_marker.setChecked(True) self.markers_group.addButton(self.single_marker) # Any marker self.any_marker = QRadioButton(self.main_page) self.any_marker.setText('any marker') self.any_marker.setObjectName('any') self.any_marker.setStyleSheet(self.style['radio button']) self.markers_group.addButton(self.any_marker) # Both methods self.both_methods = QRadioButton(self.main_page) self.both_methods.setText('both') self.both_methods.setObjectName('single any') self.both_methods.setStyleSheet(self.style['radio button']) self.markers_group.addButton(self.both_methods) # Tukey text self.tukey_text = QLabel(self.main_page) self.tukey_text.setStyleSheet(self.style['label']) # Tukey button group self.tukey_text.setText('Tukey factor:') self.tukey_group = QButtonGroup(self) # Low Tukey value self.tukey_low = QRadioButton(self.main_page) self.tukey_low.setText('1.5') self.tukey_low.setStyleSheet(self.style['radio button']) self.tukey_low.setChecked(True) self.tukey_group.addButton(self.tukey_low) # High Tukey value self.tukey_high = QRadioButton(self.main_page) self.tukey_high.setText('3.0') self.tukey_high.setStyleSheet(self.style['radio button']) self.tukey_group.addButton(self.tukey_high) # Add widgets above to analysis frame layout self.analysis_frame.layout().addWidget(self.cutoff_text) self.cutoff_buttons = QHBoxLayout() for button in self.cutoff_group.buttons(): self.cutoff_buttons.addWidget(button) self.analysis_frame.layout().addLayout(self.cutoff_buttons) self.analysis_frame.layout().addWidget(self.markers_text) self.markers_buttons = QHBoxLayout() for button in self.markers_group.buttons(): self.markers_buttons.addWidget(button) self.analysis_frame.layout().addLayout(self.markers_buttons) self.analysis_frame.layout().addWidget(self.tukey_text) self.tukey_buttons = QHBoxLayout() for button in self.tukey_group.buttons(): self.tukey_buttons.addWidget(button) self.tukey_buttons.addWidget(QLabel()) # aligns row with 2 buttons self.analysis_frame.layout().addLayout(self.tukey_buttons) # ## Output section # Output header self.output_header = QLabel(self.main_page) self.output_header.setText('Output settings') self.output_header.setStyleSheet(self.style['header']) self.output_header.adjustSize() self.main_layout.addWidget(self.output_header) # Output frame self.output_frame = QFrame(self.main_page) self.output_frame.setFrameShape(QFrame.StyledPanel) self.output_frame.setLayout(QFormLayout()) self.main_layout.addWidget(self.output_frame) # Output button self.output_button = QPushButton(self.main_page) self.output_button.setStyleSheet(self.style['button']) self.set_icon(self.output_button, 'folder') self.output_button.setObjectName('output') self.output_button.setText(' Select output folder') self.output_button.clicked.connect(self.get_path) # Output path box self.output_path = QLineEdit(self.main_page) self.output_path.setStyleSheet(self.style['line edit']) # Generate CSV checkbox self.output_csv = QCheckBox(self.main_page) self.output_csv.setText('Export multiple text files (.csv)') self.output_csv.setStyleSheet(self.style['checkbox']) self.output_csv.setChecked(True) # Generate XLSX checkbox self.output_excel = QCheckBox(self.main_page) self.output_excel.setText('Export multiple Excel spreadsheets (.xlsx)') self.output_excel.setStyleSheet(self.style['checkbox']) self.output_excel.clicked.connect(self.enable_single_excel) # Generate single, large XLSX checkbox self.single_excel = QCheckBox(self.main_page) self.single_excel.setText( 'Also save one multi-sheet Excel spreadsheet') self.single_excel.setToolTip( 'After generating all Excel spreadsheets, SCOUTS combines them into ' 'a single\nExcel spreadsheet where each sheet corresponds to an output' 'file from SCOUTS') self.single_excel.setStyleSheet(self.style['checkbox']) self.single_excel.setEnabled(False) self.single_excel.clicked.connect(self.memory_warning) # Add widgets above to output frame layout self.output_frame.layout().addRow(self.output_button, self.output_path) self.output_frame.layout().addRow(self.output_csv) self.output_frame.layout().addRow(self.output_excel) self.output_frame.layout().addRow(self.single_excel) # ## Run & help-quit section # Run button (stand-alone) self.run_button = QPushButton(self.main_page) self.set_icon(self.run_button, 'system-run') self.run_button.setText(' Run!') self.run_button.setStyleSheet(self.style['run button']) self.main_layout.addWidget(self.run_button) self.run_button.clicked.connect(self.run) # Help-quit frame (invisible) self.helpquit_frame = QFrame(self.main_page) self.helpquit_frame.setLayout(QHBoxLayout()) self.helpquit_frame.layout().setMargin(0) self.main_layout.addWidget(self.helpquit_frame) # Help button self.help_button = QPushButton(self.main_page) self.set_icon(self.help_button, 'help-about') self.help_button.setText(' Help') self.help_button.setStyleSheet(self.style['md button']) self.help_button.clicked.connect(self.get_help) # Quit button self.quit_button = QPushButton(self.main_page) self.set_icon(self.quit_button, 'process-stop') self.quit_button.setText(' Quit') self.quit_button.setStyleSheet(self.style['md button']) self.quit_button.clicked.connect(self.close) # Add widgets above to help-quit layout self.helpquit_frame.layout().addWidget(self.help_button) self.helpquit_frame.layout().addWidget(self.quit_button) # ### # ### SAMPLES PAGE # ### # Samples page layout self.samples_layout = QVBoxLayout(self.samples_page) # ## Title section # Title self.samples_title = QLabel(self.samples_page) self.samples_title.setText('Name your samples') self.samples_title.setStyleSheet(self.style['title']) self.samples_title.adjustSize() self.samples_layout.addWidget(self.samples_title) # Subtitle self.samples_subtitle = QLabel(self.samples_page) string = ( 'Please name the samples to be analysed by SCOUTS.\n\nSCOUTS searches the first ' 'column of your data\nand locates the exact string as part of the sample name.' ) self.samples_subtitle.setText(string) self.samples_subtitle.setStyleSheet(self.style['label']) self.samples_subtitle.adjustSize() self.samples_layout.addWidget(self.samples_subtitle) # ## Sample addition section # Sample addition frame self.samples_frame = QFrame(self.samples_page) self.samples_frame.setFrameShape(QFrame.StyledPanel) self.samples_frame.setLayout(QGridLayout()) self.samples_layout.addWidget(self.samples_frame) # Sample name box self.sample_name = QLineEdit(self.samples_page) self.sample_name.setStyleSheet(self.style['line edit']) self.sample_name.setPlaceholderText('Sample name ...') # Reference check self.is_reference = QCheckBox(self.samples_page) self.is_reference.setText('Reference?') self.is_reference.setStyleSheet(self.style['checkbox']) # Add sample to table self.add_sample_button = QPushButton(self.samples_page) QShortcut(QKeySequence("Return"), self.add_sample_button, self.write_to_sample_table) self.set_icon(self.add_sample_button, 'list-add') self.add_sample_button.setText(' Add sample (Enter)') self.add_sample_button.setStyleSheet(self.style['button']) self.add_sample_button.clicked.connect(self.write_to_sample_table) # Remove sample from table self.remove_sample_button = QPushButton(self.samples_page) QShortcut(QKeySequence("Delete"), self.remove_sample_button, self.remove_from_sample_table) self.set_icon(self.remove_sample_button, 'list-remove') self.remove_sample_button.setText(' Remove sample (Del)') self.remove_sample_button.setStyleSheet(self.style['button']) self.remove_sample_button.clicked.connect( self.remove_from_sample_table) # Add widgets above to sample addition layout self.samples_frame.layout().addWidget(self.sample_name, 0, 0) self.samples_frame.layout().addWidget(self.is_reference, 1, 0) self.samples_frame.layout().addWidget(self.add_sample_button, 0, 1) self.samples_frame.layout().addWidget(self.remove_sample_button, 1, 1) # ## Sample table self.sample_table = QTableWidget(self.samples_page) self.sample_table.setColumnCount(2) self.sample_table.setHorizontalHeaderItem(0, QTableWidgetItem('Sample')) self.sample_table.setHorizontalHeaderItem( 1, QTableWidgetItem('Reference?')) self.sample_table.horizontalHeader().setSectionResizeMode( 0, QHeaderView.Stretch) self.sample_table.horizontalHeader().setSectionResizeMode( 1, QHeaderView.ResizeToContents) self.samples_layout.addWidget(self.sample_table) # ## Save & clear buttons # Save & clear frame (invisible) self.saveclear_frame = QFrame(self.samples_page) self.saveclear_frame.setLayout(QHBoxLayout()) self.saveclear_frame.layout().setMargin(0) self.samples_layout.addWidget(self.saveclear_frame) # Clear samples button self.clear_samples = QPushButton(self.samples_page) self.set_icon(self.clear_samples, 'edit-delete') self.clear_samples.setText(' Clear table') self.clear_samples.setStyleSheet(self.style['md button']) self.clear_samples.clicked.connect(self.prompt_clear_data) # Save samples button self.save_samples = QPushButton(self.samples_page) self.set_icon(self.save_samples, 'document-save') self.save_samples.setText(' Save samples') self.save_samples.setStyleSheet(self.style['md button']) self.save_samples.clicked.connect(self.goto_main_page) # Add widgets above to save & clear layout self.saveclear_frame.layout().addWidget(self.clear_samples) self.saveclear_frame.layout().addWidget(self.save_samples) # ### # ### GATING PAGE # ### # Gating page layout self.gating_layout = QVBoxLayout(self.gating_page) # ## Title section # Title self.gates_title = QLabel(self.gating_page) self.gates_title.setText('Gating & outlier options') self.gates_title.setStyleSheet(self.style['title']) self.gates_title.adjustSize() self.gating_layout.addWidget(self.gates_title) # ## Gating options section # Gating header self.gate_header = QLabel(self.gating_page) self.gate_header.setText('Gating') self.gate_header.setStyleSheet(self.style['header']) self.gate_header.adjustSize() self.gating_layout.addWidget(self.gate_header) # Gating frame self.gate_frame = QFrame(self.gating_page) self.gate_frame.setFrameShape(QFrame.StyledPanel) self.gate_frame.setLayout(QFormLayout()) self.gating_layout.addWidget(self.gate_frame) # Gating button group self.gating_group = QButtonGroup(self) # Do not gate samples self.no_gates = QRadioButton(self.gating_page) self.no_gates.setObjectName('no_gate') self.no_gates.setText("Don't gate samples") self.no_gates.setStyleSheet(self.style['radio button']) self.no_gates.setChecked(True) self.gating_group.addButton(self.no_gates) self.no_gates.clicked.connect(self.activate_gate) # CyToF gating self.cytof_gates = QRadioButton(self.gating_page) self.cytof_gates.setObjectName('cytof') self.cytof_gates.setText('Mass Cytometry gating') self.cytof_gates.setStyleSheet(self.style['radio button']) self.cytof_gates.setToolTip( 'Exclude cells for which the average expression of all\n' 'markers is below the selected value') self.gating_group.addButton(self.cytof_gates) self.cytof_gates.clicked.connect(self.activate_gate) # CyToF gating spinbox self.cytof_gates_value = QDoubleSpinBox(self.gating_page) self.cytof_gates_value.setMinimum(0) self.cytof_gates_value.setMaximum(1) self.cytof_gates_value.setValue(0.1) self.cytof_gates_value.setSingleStep(0.05) self.cytof_gates_value.setEnabled(False) # scRNA-Seq gating self.rnaseq_gates = QRadioButton(self.gating_page) self.rnaseq_gates.setText('scRNA-Seq gating') self.rnaseq_gates.setStyleSheet(self.style['radio button']) self.rnaseq_gates.setToolTip( 'When calculating cutoff, ignore reads below the selected value') self.rnaseq_gates.setObjectName('rnaseq') self.gating_group.addButton(self.rnaseq_gates) self.rnaseq_gates.clicked.connect(self.activate_gate) # scRNA-Seq gating spinbox self.rnaseq_gates_value = QDoubleSpinBox(self.gating_page) self.rnaseq_gates_value.setMinimum(0) self.rnaseq_gates_value.setMaximum(10) self.rnaseq_gates_value.setValue(0) self.rnaseq_gates_value.setSingleStep(1) self.rnaseq_gates_value.setEnabled(False) # export gated population checkbox self.export_gated = QCheckBox(self.gating_page) self.export_gated.setText('Export gated cells as an output file') self.export_gated.setStyleSheet(self.style['checkbox']) self.export_gated.setEnabled(False) # Add widgets above to Gate frame layout self.gate_frame.layout().addRow(self.no_gates, QLabel()) self.gate_frame.layout().addRow(self.cytof_gates, self.cytof_gates_value) self.gate_frame.layout().addRow(self.rnaseq_gates, self.rnaseq_gates_value) self.gate_frame.layout().addRow(self.export_gated, QLabel()) # ## Outlier options section # Outlier header self.outlier_header = QLabel(self.gating_page) self.outlier_header.setText('Outliers') self.outlier_header.setStyleSheet(self.style['header']) self.outlier_header.adjustSize() self.gating_layout.addWidget(self.outlier_header) # Outlier frame self.outlier_frame = QFrame(self.gating_page) self.outlier_frame.setFrameShape(QFrame.StyledPanel) self.outlier_frame.setLayout(QVBoxLayout()) self.gating_layout.addWidget(self.outlier_frame) # Top outliers information self.top_outliers = QLabel(self.gating_page) self.top_outliers.setStyleSheet(self.style['label']) self.top_outliers.setText( 'By default, SCOUTS selects the top outliers from the population') self.top_outliers.setStyleSheet(self.style['label']) # Bottom outliers data self.bottom_outliers = QCheckBox(self.gating_page) self.bottom_outliers.setText('Include results for low outliers') self.bottom_outliers.setStyleSheet(self.style['checkbox']) # Non-outliers data self.not_outliers = QCheckBox(self.gating_page) self.not_outliers.setText('Include results for non-outliers') self.not_outliers.setStyleSheet(self.style['checkbox']) # Add widgets above to Gate frame layout self.outlier_frame.layout().addWidget(self.top_outliers) self.outlier_frame.layout().addWidget(self.bottom_outliers) self.outlier_frame.layout().addWidget(self.not_outliers) # ## Save/back button self.save_gates = QPushButton(self.gating_page) self.set_icon(self.save_gates, 'go-next') self.save_gates.setText(' Back to main menu') self.save_gates.setStyleSheet(self.style['md button']) self.gating_layout.addWidget(self.save_gates) self.save_gates.clicked.connect(self.goto_main_page) # ## Add empty label to take vertical space self.empty_label = QLabel(self.gating_page) self.empty_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.gating_layout.addWidget(self.empty_label) # ### # ### ICON SETTING # ### def set_icon(self, widget: QWidget, icon: str) -> None: """Associates an icon to a widget.""" i = QIcon() i.addPixmap( QPixmap( os.path.abspath( os.path.join(self.rootdir, 'src', 'default_icons', f'{icon}.svg')))) widget.setIcon(QIcon.fromTheme(icon, i)) # ### # ### STACKED WIDGET PAGE SWITCHING # ### def goto_main_page(self) -> None: """Switches stacked widget pages to the main page.""" self.stacked_pages.setCurrentWidget(self.main_page) def goto_samples_page(self) -> None: """Switches stacked widget pages to the samples table page.""" self.stacked_pages.setCurrentWidget(self.samples_page) def goto_gates_page(self) -> None: """Switches stacked widget pages to the gating & other options page.""" self.stacked_pages.setCurrentWidget(self.gating_page) # ### # ### MAIN PAGE GUI LOGIC # ### def get_path(self) -> None: """Opens a dialog box and sets the chosen file/folder path, depending on the caller widget.""" options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog sender_name = self.sender().objectName() if sender_name == 'input': query, _ = QFileDialog.getOpenFileName(self, "Select file", "", "All Files (*)", options=options) elif sender_name == 'output': query = QFileDialog.getExistingDirectory(self, "Select Directory", options=options) else: return if query: getattr(self, f'{sender_name}_path').setText(query) def enable_single_excel(self) -> None: """Enables checkbox for generating a single Excel output.""" if self.output_excel.isChecked(): self.single_excel.setEnabled(True) else: self.single_excel.setEnabled(False) self.single_excel.setChecked(False) # ### # ### SAMPLE NAME/SAMPLE TABLE GUI LOGIC # ### def write_to_sample_table(self) -> None: """Writes data to sample table.""" table = self.sample_table ref = 'no' sample = self.sample_name.text() if sample: for cell in range(table.rowCount()): item = table.item(cell, 0) if item.text() == sample: self.same_sample() return if self.is_reference.isChecked(): for cell in range(table.rowCount()): item = table.item(cell, 1) if item.text() == 'yes': self.more_than_one_reference() return ref = 'yes' sample = QTableWidgetItem(sample) is_reference = QTableWidgetItem(ref) is_reference.setFlags(Qt.ItemIsEnabled) row_position = table.rowCount() table.insertRow(row_position) table.setItem(row_position, 0, sample) table.setItem(row_position, 1, is_reference) self.is_reference.setChecked(False) self.sample_name.setText('') def remove_from_sample_table(self) -> None: """Removes data from sample table.""" table = self.sample_table rows = set(index.row() for index in table.selectedIndexes()) for index in sorted(rows, reverse=True): self.sample_table.removeRow(index) def prompt_clear_data(self) -> None: """Prompts option to clear all data in the sample table.""" if self.confirm_clear_data(): table = self.sample_table while table.rowCount(): self.sample_table.removeRow(0) # ### # ### GATING GUI LOGIC # ### def activate_gate(self) -> None: """Activates/deactivates buttons related to gating.""" if self.sender().objectName() == 'no_gate': self.cytof_gates_value.setEnabled(False) self.rnaseq_gates_value.setEnabled(False) self.export_gated.setEnabled(False) self.export_gated.setChecked(False) elif self.sender().objectName() == 'cytof': self.cytof_gates_value.setEnabled(True) self.rnaseq_gates_value.setEnabled(False) self.export_gated.setEnabled(True) elif self.sender().objectName() == 'rnaseq': self.cytof_gates_value.setEnabled(False) self.rnaseq_gates_value.setEnabled(True) self.export_gated.setEnabled(True) # ### # ### CONNECT SCOUTS TO ANALYTICAL MODULES # ### def run(self) -> None: """Runs SCOUTS as a Worker, based on user input in the GUI.""" try: data = self.parse_input() except Exception as error: trace = traceback.format_exc() self.propagate_error((error, trace)) else: data['widget'] = self worker = Worker(func=start_scouts, **data) worker.signals.started.connect(self.analysis_has_started) worker.signals.finished.connect(self.analysis_has_finished) worker.signals.success.connect(self.success_message) worker.signals.error.connect(self.propagate_error) self.threadpool.start(worker) def parse_input(self) -> Dict: """Returns user input on the GUI as a dictionary.""" # Input and output input_dict = { 'input_file': str(self.input_path.text()), 'output_folder': str(self.output_path.text()) } if not input_dict['input_file'] or not input_dict['output_folder']: raise NoIOPathError # Set cutoff by reference or by sample rule input_dict['cutoff_rule'] = self.cutoff_group.checkedButton( ).objectName() # 'sample', 'ref', 'sample ref' # Outliers for each individual marker or any marker in row input_dict['marker_rule'] = self.markers_group.checkedButton( ).objectName() # 'single', 'any', 'single any' # Tukey factor used for calculating cutoff input_dict['tukey_factor'] = float( self.tukey_group.checkedButton().text()) # '1.5', '3.0' # Output settings input_dict['export_csv'] = True if self.output_csv.isChecked( ) else False input_dict['export_excel'] = True if self.output_excel.isChecked( ) else False input_dict['single_excel'] = True if self.single_excel.isChecked( ) else False # Retrieve samples from sample table input_dict['sample_list'] = [] for tuples in self.yield_samples_from_table(): input_dict['sample_list'].append(tuples) if not input_dict['sample_list']: raise NoSampleError # Set gate cutoff (if any) input_dict['gating'] = self.gating_group.checkedButton().objectName( ) # 'no_gate', 'cytof', 'rnaseq' input_dict['gate_cutoff_value'] = None if input_dict['gating'] != 'no_gate': input_dict['gate_cutoff_value'] = getattr( self, f'{input_dict["gating"]}_gates_value').value() input_dict['export_gated'] = True if self.export_gated.isChecked( ) else False # Generate results for non-outliers input_dict['non_outliers'] = False if self.not_outliers.isChecked(): input_dict['non_outliers'] = True # Generate results for bottom outliers input_dict['bottom_outliers'] = False if self.bottom_outliers.isChecked(): input_dict['bottom_outliers'] = True # return dictionary with all gathered inputs return input_dict def yield_samples_from_table( self) -> Generator[Tuple[str, str], None, None]: """Yields sample names from the sample table.""" table = self.sample_table for cell in range(table.rowCount()): sample_name = table.item(cell, 0).text() sample_type = table.item(cell, 1).text() yield sample_name, sample_type # ### # ### MESSAGE BOXES # ### def analysis_has_started(self) -> None: """Disables run button while SCOUTS analysis is underway.""" self.run_button.setText(' Working...') self.run_button.setEnabled(False) def analysis_has_finished(self) -> None: """Enables run button after SCOUTS analysis has finished.""" self.run_button.setEnabled(True) self.run_button.setText(' Run!') def success_message(self) -> None: """Info message box used when SCOUTS finished without errors.""" title = "Analysis finished!" mes = "Your analysis has finished. No errors were reported." if self.stacked_pages.isEnabled() is True: QMessageBox.information(self, title, mes) def memory_warning(self) -> None: """Warning message box used when user wants to generate a single excel file.""" if self.sender().isChecked(): title = 'Memory warning!' mes = ( "Depending on your dataset, this option can consume a LOT of memory and take" " a long time to process. Please make sure that your computer can handle it!" ) QMessageBox.information(self, title, mes) def same_sample(self) -> None: """Error message box used when the user tries to input the same sample twice in the sample table.""" title = 'Error: sample name already in table' mes = ( "Sorry, you can't do this because this sample name is already in the table. " "Please select a different name.") QMessageBox.critical(self, title, mes) def more_than_one_reference(self) -> None: """Error message box used when the user tries to input two reference samples in the sample table.""" title = "Error: more than one reference selected" mes = ( "Sorry, you can't do this because there is already a reference column in the table. " "Please remove it before adding a reference.") QMessageBox.critical(self, title, mes) def confirm_clear_data(self) -> bool: """Question message box used to confirm user action of clearing sample table.""" title = 'Confirm Action' mes = "Table will be cleared. Are you sure?" reply = QMessageBox.question(self, title, mes, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: return True return False # ### # ### EXCEPTIONS & ERRORS # ### def propagate_error(self, error: Tuple[Exception, str]) -> None: """Calls the appropriate error message box based on type of Exception raised.""" if isinstance(error[0], NoIOPathError): self.no_io_path_error_message() elif isinstance(error[0], NoReferenceError): self.no_reference_error_message() elif isinstance(error[0], NoSampleError): self.no_sample_error_message() elif isinstance(error[0], PandasInputError): self.pandas_input_error_message() elif isinstance(error[0], SampleNamingError): self.sample_naming_error_message() else: self.generic_error_message(error) def no_io_path_error_message(self) -> None: """Message displayed when the user did not include an input file path, or an output folder path.""" title = 'Error: no file/folder' message = ("Sorry, no input file and/or output folder was provided. " "Please add the path to the necessary file/folder.") QMessageBox.critical(self, title, message) def no_reference_error_message(self) -> None: """Message displayed when the user wants to analyse cutoff based on a reference, but did not specify what sample corresponds to the reference.""" title = "Error: No reference selected" message = ( "Sorry, no reference sample was found on the sample list, but analysis was set to " "reference. Please add a reference sample, or change the rule for cutoff calculation." ) QMessageBox.critical(self, title, message) def no_sample_error_message(self) -> None: """Message displayed when the user did not add any samples to the sample table.""" title = "Error: No samples selected" message = ( "Sorry, the analysis cannot be performed because no sample names were input. " "Please add your sample names.") QMessageBox.critical(self, title, message) def pandas_input_error_message(self) -> None: """Message displayed when the input file cannot be read (likely because it is not a Excel or csv file).""" title = 'Error: unexpected input file' message = ( "Sorry, the input file could not be read. Please make sure that " "the data is save in a valid format (supported formats are: " ".csv, .xlsx).") QMessageBox.critical(self, title, message) def sample_naming_error_message(self) -> None: """Message displayed when none of the sample names passed by the user are found in the input DataFrame.""" title = 'Error: sample names not in input file' message = ( "Sorry, your sample names were not found in the input file. Please " "make sure that the names were typed correctly (case-sensitive).") QMessageBox.critical(self, title, message) def generic_error_message(self, error: Tuple[Exception, str]) -> None: """Error message box used to display any error message (including traceback) for any uncaught errors.""" title = 'An error occurred!' name, trace = error QMessageBox.critical(self, title, f"{str(name)}\n\nfull traceback:\n{trace}") def not_implemented_error_message(self) -> None: """Error message box used when the user accesses a functionality that hasn't been implemented yet.""" title = "Not yet implemented" mes = "Sorry, this functionality has not been implemented yet." QMessageBox.critical(self, title, mes) # ### # ### HELP & QUIT # ### @staticmethod def get_help() -> None: """Opens SCOUTS documentation on the browser. Called when the user clicks the "help" button""" webbrowser.open('https://scouts.readthedocs.io/en/master/') def closeEvent(self, event: QEvent) -> None: """Defines the message box for when the user wants to quit SCOUTS.""" title = 'Quit SCOUTS' mes = "Are you sure you want to quit?" reply = QMessageBox.question(self, title, mes, QMessageBox.Yes | QMessageBox.No, QMessageBox.No) if reply == QMessageBox.Yes: self.stacked_pages.setEnabled(False) message = self.quit_message() waiter = Waiter(waiter_func=self.threadpool.activeThreadCount) waiter.signals.started.connect(message.show) waiter.signals.finished.connect(message.destroy) waiter.signals.finished.connect(sys.exit) self.threadpool.start(waiter) event.ignore() def quit_message(self) -> QDialog: """Displays a window while SCOUTS is exiting""" message = QDialog(self) message.setWindowTitle('Exiting SCOUTS') message.resize(300, 50) label = QLabel('SCOUTS is exiting, please wait...', message) label.setStyleSheet(self.style['label']) label.adjustSize() label.setAlignment(Qt.AlignCenter) label.move(int((message.width() - label.width()) / 2), int((message.height() - label.height()) / 2)) return message
class BreakpointDialog(QDialog): """ Dialog to edit breakpoints. """ def __init__(self, breakpoint_: Breakpoint, workspace: 'Workspace', parent=None): super().__init__(parent) self.breakpoint = breakpoint_ self.workspace = workspace self.setWindowTitle('Edit Breakpoint') self.main_layout: QVBoxLayout = QVBoxLayout() self._type_radio_group: Optional[QButtonGroup] = None self._address_box: Optional[QAddressInput] = None self._size_box: Optional[QLineEdit] = None self._comment_box: Optional[QLineEdit] = None self._status_label: Optional[QLabel] = None self._ok_button: Optional[QPushButton] = None self._init_widgets() self.setLayout(self.main_layout) self._validate() # # Private methods # def _init_widgets(self): layout = QGridLayout() self.main_layout.addLayout(layout) self._status_label = QLabel(self) row = 0 layout.addWidget(QLabel('Break on:', self), row, 0, Qt.AlignRight) self._type_radio_group = QButtonGroup(self) self._type_radio_group.addButton(QRadioButton('Execute', self), BreakpointType.Execute.value) self._type_radio_group.addButton(QRadioButton('Write', self), BreakpointType.Write.value) self._type_radio_group.addButton(QRadioButton('Read', self), BreakpointType.Read.value) for b in self._type_radio_group.buttons(): layout.addWidget(b, row, 1) row += 1 self._type_radio_group.button( self.breakpoint.type.value).setChecked(True) layout.addWidget(QLabel('Address:', self), row, 0, Qt.AlignRight) self._address_box = QAddressInput(self._on_address_changed, self.workspace, parent=self, default=f'{self.breakpoint.addr:#x}') layout.addWidget(self._address_box, row, 1) row += 1 layout.addWidget(QLabel('Size:', self), row, 0, Qt.AlignRight) self._size_box = QLineEdit(self) self._size_box.setText(f'{self.breakpoint.size:#x}') self._size_box.textChanged.connect(self._on_size_changed) layout.addWidget(self._size_box, row, 1) row += 1 layout.addWidget(QLabel('Comment:', self), row, 0, Qt.AlignRight) self._comment_box = QLineEdit(self) self._comment_box.setText(self.breakpoint.comment) layout.addWidget(self._comment_box, row, 1) row += 1 self.main_layout.addWidget(self._status_label) buttons = QDialogButtonBox(parent=self) buttons.setStandardButtons(QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Ok) buttons.accepted.connect(self._on_ok_clicked) buttons.rejected.connect(self.close) self._ok_button = buttons.button(QDialogButtonBox.Ok) self._ok_button.setEnabled(False) self.main_layout.addWidget(buttons) def _set_valid(self, valid: bool): if not valid: self._status_label.setText('Invalid') self._status_label.setProperty('class', 'status_invalid') else: self._status_label.setText('Valid') self._status_label.setProperty('class', 'status_valid') self._ok_button.setEnabled(valid) self._status_label.style().unpolish(self._status_label) self._status_label.style().polish(self._status_label) def _get_size(self): try: return int(self._size_box.text(), 0) except ValueError: pass return None # # Event handlers # def _validate(self): self._set_valid( bool(self._address_box.target is not None and self._get_size())) def _on_address_changed(self, new_text): # pylint: disable=unused-argument self._validate() def _on_size_changed(self, new_text): # pylint: disable=unused-argument self._validate() def _on_ok_clicked(self): self.breakpoint.type = BreakpointType( self._type_radio_group.checkedId()) self.breakpoint.addr = self._address_box.target self.breakpoint.size = self._get_size() self.breakpoint.comment = self._comment_box.text() self.workspace.instance.breakpoint_mgr.breakpoints.am_event() self.accept()
class MyWidget(QWidget): def __init__(self, parent): super().__init__(parent) self.buttons_id_value = { 1: ('comma', ','), 2: ('space', '\b'), 3: ('tab', '\t'), 4: ('semicolon', ';') } self.separator = QButtonGroup() lab = QLabel() lab.setText('Choose a separator:') for bid, value in self.buttons_id_value.items(): self.separator.addButton(QRadioButton(value[0]), id=bid) self.separator.setExclusive(True) self.default_button = self.separator.button(1) button_layout = QHBoxLayout() for button in self.separator.buttons(): button_layout.addWidget(button) self.default_button.click() openFileChooser = QPushButton('Choose') fileChooser = QFileDialog(self, 'Open csv', str(os.getcwd()), 'Csv (*.csv *.tsv *.dat)') fileChooser.setFileMode(QFileDialog.ExistingFile) self.filePath = QLineEdit() openFileChooser.released.connect(fileChooser.show) fileChooser.fileSelected.connect(self.filePath.setText) self.filePath.textChanged.connect(self.checkFileExists) nameLabel = QLabel('Select a name:', self) self.nameField = QLineEdit(self) self.nameErrorLabel = QLabel(self) self.file_layout = QVBoxLayout() fileChooserLayout = QHBoxLayout() nameRowLayout = QHBoxLayout() fileChooserLayout.addWidget(openFileChooser) fileChooserLayout.addWidget(self.filePath) nameRowLayout.addWidget(nameLabel) nameRowLayout.addWidget(self.nameField) self.fileErrorLabel = QLabel(self) self.file_layout.addLayout(fileChooserLayout) self.file_layout.addWidget(self.fileErrorLabel) self.file_layout.addLayout(nameRowLayout) self.file_layout.addWidget(self.nameErrorLabel) self.fileErrorLabel.hide() self.nameErrorLabel.hide() self.tablePreview = SearchableAttributeTableWidget(self, True) self.tableSpinner = QtWaitingSpinner( self.tablePreview, centerOnParent=True, disableParentWhenSpinning=True) self.nameField.textEdited.connect(self.nameErrorLabel.hide) # Split file by row splitRowLayout = QHBoxLayout() self.checkSplit = QCheckBox('Split file by rows', self) self.numberRowsChunk = QLineEdit(self) self.numberRowsChunk.setPlaceholderText( 'Number of rows per chunk') self.numberRowsChunk.setValidator(QIntValidator(self)) splitRowLayout.addWidget(self.checkSplit) splitRowLayout.addWidget(self.numberRowsChunk) self.checkSplit.stateChanged.connect(self.toggleSplitRows) layout = QVBoxLayout() layout.addLayout(self.file_layout) layout.addWidget(lab) layout.addLayout(button_layout) layout.addLayout(splitRowLayout) layout.addWidget(QLabel('Preview')) layout.addWidget(self.tablePreview) self.setLayout(layout) self.filePath.textChanged.connect(self.loadPreview) self.separator.buttonClicked.connect(self.loadPreview) @Slot(object) def loadPreview(self) -> None: if not os.path.isfile(self.filePath.text()): return class WorkerThread(QThread): resultReady = Signal(Frame) def __init__(self, path: str, separ: str, parent=None): super().__init__(parent) self.__path = path self.__sep = separ def run(self): header = pd.read_csv(self.__path, sep=self.__sep, index_col=False, nrows=0) self.resultReady.emit(Frame(header)) sep: int = self.separator.checkedId() sep_s: str = self.buttons_id_value[sep][ 1] if sep != -1 else None assert sep_s is not None # Async call to load header worker = WorkerThread(path=self.filePath.text(), separ=sep_s, parent=self) worker.resultReady.connect(self.onPreviewComputed) worker.finished.connect(worker.deleteLater) self.tableSpinner.start() worker.start() @Slot(Frame) def onPreviewComputed(self, header: Frame): self.tablePreview.setSourceFrameModel(FrameModel(self, header)) self.tablePreview.model().setAllChecked(True) self.tableSpinner.stop() @Slot(str) def checkFileExists(self, path: str) -> None: file_exists = os.path.isfile(path) if not file_exists: self.fileErrorLabel.setText('File does not exists!') self.fileErrorLabel.setStyleSheet('color: red') self.filePath.setToolTip('File does not exists!') self.filePath.setStyleSheet('border: 1px solid red') # self.file_layout.addWidget(self.fileErrorLabel) self.parentWidget().disableOkButton() self.fileErrorLabel.show() else: # self.file_layout.removeWidget(self.fileErrorLabel) self.fileErrorLabel.hide() self.filePath.setStyleSheet('') self.parentWidget().enableOkButton() if not self.nameField.text(): name: str = os.path.splitext(os.path.basename(path))[0] self.nameField.setText(name) @Slot(Qt.CheckState) def toggleSplitRows(self, state: Qt.CheckState) -> None: if state == Qt.Checked: self.numberRowsChunk.setEnabled(True) else: self.numberRowsChunk.setDisabled(True) def showNameError(self, msg: str) -> None: self.nameErrorLabel.setText(msg) self.nameErrorLabel.setStyleSheet('color: red') self.nameErrorLabel.show()
class PreferencesDocumentPresetsPage(QWidget): preferencesChanged = Signal() def __init__(self, parent=None): super().__init__(parent) # Title title = QLabel( self.tr("<strong style=\"font-size:large;\">{0}</strong>").format( self.title())) # # Content: Header Labels rdbDefaultHeaderLabelHorizontalLetters = QRadioButton( self.tr("Letters")) rdbDefaultHeaderLabelHorizontalLetters.setToolTip( self. tr("Capital letters as default horizontal header labels of new documents" )) rdbDefaultHeaderLabelHorizontalNumbers = QRadioButton( self.tr("Numbers")) rdbDefaultHeaderLabelHorizontalNumbers.setToolTip( self. tr("Decimal numbers as default horizontal header labels of new documents" )) self._grpDefaultHeaderLabelHorizontal = QButtonGroup(self) self._grpDefaultHeaderLabelHorizontal.addButton( rdbDefaultHeaderLabelHorizontalLetters, Preferences.HeaderLabel.Letter.value) self._grpDefaultHeaderLabelHorizontal.addButton( rdbDefaultHeaderLabelHorizontalNumbers, Preferences.HeaderLabel.Decimal.value) self._grpDefaultHeaderLabelHorizontal.buttonClicked.connect( self._onPreferencesChanged) defaultHeaderLabelHorizontalBox = QHBoxLayout() defaultHeaderLabelHorizontalBox.addWidget( rdbDefaultHeaderLabelHorizontalLetters) defaultHeaderLabelHorizontalBox.addWidget( rdbDefaultHeaderLabelHorizontalNumbers) rdbDefaultHeaderLabelVerticalLetters = QRadioButton(self.tr("Letters")) rdbDefaultHeaderLabelVerticalLetters.setToolTip( self. tr("Capital letters as default vertical header labels of new documents" )) rdbDefaultHeaderLabelVerticalNumbers = QRadioButton(self.tr("Numbers")) rdbDefaultHeaderLabelVerticalNumbers.setToolTip( self. tr("Decimal numbers as default vertical header labels of new documents" )) self._grpDefaultHeaderLabelVertical = QButtonGroup() self._grpDefaultHeaderLabelVertical.addButton( rdbDefaultHeaderLabelVerticalLetters, Preferences.HeaderLabel.Letter.value) self._grpDefaultHeaderLabelVertical.addButton( rdbDefaultHeaderLabelVerticalNumbers, Preferences.HeaderLabel.Decimal.value) self._grpDefaultHeaderLabelVertical.buttonClicked.connect( self._onPreferencesChanged) defaultHeaderLabelVerticalBox = QHBoxLayout() defaultHeaderLabelVerticalBox.addWidget( rdbDefaultHeaderLabelVerticalLetters) defaultHeaderLabelVerticalBox.addWidget( rdbDefaultHeaderLabelVerticalNumbers) defaultHeaderLabelLayout = QFormLayout() defaultHeaderLabelLayout.addRow( self.tr("Labels of the horizontal header"), defaultHeaderLabelHorizontalBox) defaultHeaderLabelLayout.addRow( self.tr("Labels of the vertical header"), defaultHeaderLabelVerticalBox) defaultHeaderLabelGroup = QGroupBox(self.tr("Header Labels")) defaultHeaderLabelGroup.setLayout(defaultHeaderLabelLayout) # # Content: Cell Counts self._spbDefaultCellCountColumn = QSpinBox() self._spbDefaultCellCountColumn.setRange(1, 1000) self._spbDefaultCellCountColumn.setToolTip( self.tr("Default number of columns of new documents")) self._spbDefaultCellCountColumn.valueChanged.connect( self._onPreferencesChanged) self._spbDefaultCellCountRow = QSpinBox() self._spbDefaultCellCountRow.setRange(1, 1000) self._spbDefaultCellCountRow.setToolTip( self.tr("Default number of rows of new documents")) self._spbDefaultCellCountRow.valueChanged.connect( self._onPreferencesChanged) defaultCellCountLayout = QFormLayout() defaultCellCountLayout.addRow(self.tr("Number of columns"), self._spbDefaultCellCountColumn) defaultCellCountLayout.addRow(self.tr("Number of rows"), self._spbDefaultCellCountRow) defaultCellCountGroup = QGroupBox(self.tr("Cell Counts")) defaultCellCountGroup.setLayout(defaultCellCountLayout) # Main layout self._layout = QVBoxLayout(self) self._layout.addWidget(title) self._layout.addWidget(defaultHeaderLabelGroup) self._layout.addWidget(defaultCellCountGroup) self._layout.addStretch(1) def setZeroMargins(self): self._layout.setContentsMargins(0, 0, 0, 0) def title(self): return self.tr("Document Presets") def _onPreferencesChanged(self): self.preferencesChanged.emit() def setDefaultHeaderLabelHorizontal(self, type): if type.value != self._grpDefaultHeaderLabelHorizontal.checkedId(): self._onPreferencesChanged() for button in self._grpDefaultHeaderLabelHorizontal.buttons(): if self._grpDefaultHeaderLabelHorizontal.id(button) == type.value: button.setChecked(True) def defaultHeaderLabelHorizontal(self): return Preferences.HeaderLabel( self._grpDefaultHeaderLabelHorizontal.checkedId()) def setDefaultHeaderLabelVertical(self, type): if type.value != self._grpDefaultHeaderLabelVertical.checkedId(): self._onPreferencesChanged() for button in self._grpDefaultHeaderLabelVertical.buttons(): if self._grpDefaultHeaderLabelVertical.id(button) == type.value: button.setChecked(True) def defaultHeaderLabelVertical(self): return Preferences.HeaderLabel( self._grpDefaultHeaderLabelVertical.checkedId()) def setDefaultCellCountColumn(self, val): self._spbDefaultCellCountColumn.setValue(val) def defaultCellCountColumn(self): return self._spbDefaultCellCountColumn.value() def setDefaultCellCountRow(self, val): self._spbDefaultCellCountRow.setValue(val) def defaultCellCountRow(self): return self._spbDefaultCellCountRow.value()