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 Natang_mayammd(QWidget): ''' mayaからmmdモデルへエクスポートするためのウィンドウ ''' def __init__(self, parent): QWidget.__init__(self) self.parent = parent self.setWindowFlags(Qt.WindowStaysOnTopHint) self.setWindowTitle('~ Maya > MMD ~') self.setStyleSheet('font-size: 15px; color: #ddf;') vbl = QVBoxLayout() self.setLayout(vbl) self.file_khatangton = os.path.join(os.path.dirname(__file__), 'asset', 'khatangton2.txt') try: with open(self.file_khatangton, 'r', encoding='utf-8') as f: chue_tem_file = f.readline().split('=')[-1].strip() satsuan = f.readline().split('=')[-1].strip() chai_bs = int(f.readline().split('=')[-1].strip()) chai_kraduk = int(f.readline().split('=')[-1].strip()) chai_watsadu = int(f.readline().split('=')[-1].strip()) lok_tex = int(f.readline().split('=')[-1].strip()) thangmot = int(f.readline().split('=')[-1].strip()) pit_mai = int(f.readline().split('=')[-1].strip()) except: chue_tem_file = '' satsuan = '0.125' chai_bs = True chai_kraduk = True chai_watsadu = True lok_tex = False thangmot = False pit_mai = True hbl = QHBoxLayout() vbl.addLayout(hbl) hbl.addWidget(QLabel('ファイル')) self.le_chue_file = QLineEdit(chue_tem_file) hbl.addWidget(self.le_chue_file) self.le_chue_file.setFixedWidth(300) self.le_chue_file.textChanged.connect(self.chue_thuk_kae) self.btn_khon_file = QPushButton('...') hbl.addWidget(self.btn_khon_file) self.btn_khon_file.clicked.connect(self.khon_file) hbl = QHBoxLayout() vbl.addLayout(hbl) hbl.addWidget(QLabel('尺度')) self.le_satsuan = QLineEdit(satsuan) hbl.addWidget(self.le_satsuan) self.le_satsuan.setFixedWidth(100) self.le_satsuan.textEdited.connect(self.satsuan_thuk_kae) hbl.addWidget(QLabel('×')) hbl.addStretch() self.cb_chai_kraduk = QCheckBox('骨も作る') vbl.addWidget(self.cb_chai_kraduk) self.cb_chai_kraduk.setChecked(chai_kraduk) self.cb_chai_bs = QCheckBox('モーフも作る') vbl.addWidget(self.cb_chai_bs) self.cb_chai_bs.setChecked(chai_bs) self.cb_chai_watsadu = QCheckBox('材質を使う') vbl.addWidget(self.cb_chai_watsadu) self.cb_chai_watsadu.setChecked(chai_watsadu) self.cb_lok_tex = QCheckBox('テクスチャファイルをコピーする') vbl.addWidget(self.cb_lok_tex) self.cb_lok_tex.setChecked(lok_tex) hbl = QHBoxLayout() vbl.addLayout(hbl) hbl.addWidget(QLabel('使うポリゴン')) self.btng = QButtonGroup() self.rb_thangmot = QRadioButton('全部') hbl.addWidget(self.rb_thangmot) self.btng.addButton(self.rb_thangmot) self.rb_thilueak = QRadioButton('選択されている') hbl.addWidget(self.rb_thilueak) self.btng.addButton(self.rb_thilueak) hbl.addStretch() if (thangmot): self.rb_thangmot.setChecked(True) else: self.rb_thilueak.setChecked(True) hbl = QHBoxLayout() vbl.addLayout(hbl) hbl.addStretch() self.btn_roem_sang = QPushButton('作成開始') hbl.addWidget(self.btn_roem_sang) self.btn_roem_sang.clicked.connect(self.roem_sang) self.btn_roem_sang.setFixedSize(220, 50) self.chue_thuk_kae(self.le_chue_file.text()) self.cb_pit = QCheckBox('終わったらこの\nウィンドウを閉じる') hbl.addWidget(self.cb_pit) self.cb_pit.setChecked(pit_mai) def khon_file(self): self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint) chue_file, ok = QFileDialog.getSaveFileName(filter='PMX (*.pmx)') if (ok): self.le_chue_file.setText(chue_file) self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) self.show() def chue_thuk_kae(self, chue_file): sakun = chue_file.split('.')[-1] sang_dai = (sakun.lower() == 'pmx') self.btn_roem_sang.setEnabled(sang_dai) self.btn_roem_sang.setStyleSheet( ['text-decoration: line-through; color: #aab;', ''][sang_dai]) def satsuan_thuk_kae(self, kha): try: float(kha) except: self.le_satsuan.setText('1') def roem_sang(self): chue_tem_file = self.le_chue_file.text() try: satsuan = float(self.le_satsuan.text()) except: self.le_satsuan.setText('1') satsuan = 1. chai_watsadu = self.cb_chai_watsadu.isChecked() chai_bs = self.cb_chai_bs.isChecked() chai_kraduk = self.cb_chai_kraduk.isChecked() lok_tex = self.cb_lok_tex.isChecked() thangmot = self.btng.checkedButton() == self.rb_thangmot mayapaipmx.sang(chue_tem_file, satsuan, chai_kraduk, chai_bs, chai_watsadu, lok_tex, thangmot) pit_mai = self.cb_pit.isChecked() with open(self.file_khatangton, 'w', encoding='utf-8') as f: f.write('ファイルの名前 = %s\n' % chue_tem_file) f.write('尺度 = %f\n' % satsuan) f.write('ブレンドシェープ = %d\n' % chai_bs) f.write('ジョイント = %d\n' % chai_kraduk) f.write('材質 = %d\n' % chai_watsadu) f.write('テクスチャのコピー = %d\n' % lok_tex) f.write('ポリゴン全部 = %d\n' % thangmot) f.write('閉じる = %d\n' % pit_mai) if (pit_mai): self.close() def keyPressEvent(self, e): if (e.key() == Qt.Key_Escape): self.close() def closeEvent(self, e): self.parent.natangyoi['mayammd'] = None
class SettingsWindow(QDialog): """Settings menu with two tabs for settings models and components""" def __init__(self, master, enigma_api): """ Submenu for setting Enigma model and component settings :param master: Qt parent object :param enigma_api: {EnigmaAPI} """ super().__init__(master) # QT WINDOW SETTINGS =================================================== main_layout = QVBoxLayout(self) self.__settings_frame = QFrame(self) self.__settings_layout = QHBoxLayout(self.__settings_frame) self.setWindowTitle("Settings") self.setLayout(main_layout) self.setFixedHeight(620) self.__reflector_group = [] self.__rotor_frames = [] # SAVE ATTRIBUTES ====================================================== self.__enigma_api = enigma_api self.__rotor_selectors = [] self.__ring_selectors = [] self.__ukwd_window = UKWDSettingsWindow(self, enigma_api) # ROTORS AND REFLECTOR SETTINGS ======================================== self.__ukwd_button = QPushButton("UKW-D pairs") self.__ukwd_button.clicked.connect(self.open_ukwd_window) # TAB WIDGET =========================================================== tab_widget = QTabWidget() self.__stacked_wikis = _ViewSwitcherWidget(self, self.regenerate_for_model) tab_widget.addTab(self.__stacked_wikis, "Enigma model") tab_widget.addTab(self.__settings_frame, "Component settings") # BUTTONS ============================================================== button_frame = QFrame(self) button_layout = QHBoxLayout(button_frame) button_layout.setAlignment(Qt.AlignRight) self.__apply_btn = QPushButton("Apply") self.__apply_btn.clicked.connect(self.collect) storno = QPushButton("Storno") storno.clicked.connect(self.close) button_layout.addWidget(storno) button_layout.addWidget(self.__apply_btn) # SHOW WIDGETS ========================================================= model_i = list(VIEW_DATA.keys()).index(self.__enigma_api.model()) self.__stacked_wikis.select_model(model_i) self.__stacked_wikis.highlight(model_i) main_layout.addWidget(tab_widget) main_layout.addWidget(button_frame) def open_ukwd_window(self): """Opens UKWD wiring menu""" logging.info("Opened UKW-D wiring menu...") self.__ukwd_window.exec_() self.refresh_ukwd() def refresh_ukwd(self): """Refreshes Apply button according to criteria (UKW-D rotor must be selected to edit its settings)""" if self.__reflector_group.checkedButton().text() == "UKW-D": logging.info("UKW-D reflector selected, enabling UKW-D button...") if len(self.__ukwd_window.pairs()) != 12: self.__apply_btn.setDisabled(True) self.__apply_btn.setToolTip( "Connect all 12 pairs in UKW-D wiring!") else: self.__apply_btn.setDisabled(False) self.__apply_btn.setToolTip(None) self.__ukwd_button.setDisabled(False) self.__ukwd_button.setToolTip( "Select the UKW-D rotor to edit settings") if len(self.__rotor_frames) == 4: # IF THIN ROTORS logging.info("Disabling thin rotor radiobuttons...") self.__rotor_frames[0].setDisabled(True) else: logging.info( "UKW-D reflector deselected, disabling UKW-D button...") self.__apply_btn.setDisabled(False) self.__apply_btn.setToolTip(None) self.__ukwd_button.setDisabled(True) self.__ukwd_button.setToolTip(None) if len(self.__rotor_frames) == 4: # IF THIN ROTORS logging.info("Enabling thin rotor radiobuttons...") self.__rotor_frames[0].setDisabled(False) def generate_components(self, reflectors, rotors, rotor_n, charset): """Generates currently displayed components based on Enigma model :param reflectors: {str} Reflector labels :param rotors: {[str, str, str]} Rotor labels :param rotor_n: {int} Number of rotors the Enigma model has """ # REFLECTOR SETTINGS =================================================== spacing = 15 style = "font-size: 18px; text-align: center;" reflector_frame = QFrame(self.__settings_frame) reflector_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) reflector_layout = QVBoxLayout(reflector_frame) reflector_layout.setSpacing(spacing) reflector_layout.addWidget( QLabel("REFLECTOR", reflector_frame, styleSheet=style), alignment=Qt.AlignHCenter, ) self.__reflector_group = QButtonGroup(reflector_frame) reflector_layout.setAlignment(Qt.AlignTop) for i, model in enumerate(reflectors): radio = QRadioButton(model, reflector_frame) radio.setToolTip( "Reflector is an Enigma component that \nreflects " "letters from the rotors back to the lightboard") self.__reflector_group.addButton(radio) self.__reflector_group.setId(radio, i) reflector_layout.addWidget(radio, alignment=Qt.AlignTop) reflector_layout.addStretch() reflector_layout.addWidget(self.__ukwd_button) self.__reflector_group.button(0).setChecked(True) self.__reflector_group.buttonClicked.connect(self.refresh_ukwd) self.__settings_layout.addWidget(reflector_frame) # ROTOR SETTINGS ======================================================= self.__rotor_selectors = [] self.__ring_selectors = [] self.__rotor_frames = [] for rotor in range(rotor_n): rotor_frame = QFrame(self.__settings_frame) rotor_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Plain) rotor_layout = QVBoxLayout(rotor_frame) rotor_layout.setAlignment(Qt.AlignTop) rotor_layout.setSpacing(spacing) rotor_frame.setLayout(rotor_layout) # ROTOR RADIOS ===================================================== label = QLabel(SELECTOR_LABELS[-rotor_n:][rotor], rotor_frame, styleSheet=style) label.setToolTip(SELECTOR_TOOLTIPS[-rotor_n:][rotor]) rotor_layout.addWidget(label, alignment=Qt.AlignHCenter) button_group = QButtonGroup(rotor_frame) final_rotors = rotors if "Beta" in rotors: logging.info( "Enigma M4 rotors detected, adjusting radiobuttons...") if rotor == 0: final_rotors = ["Beta", "Gamma"] else: final_rotors.remove("Beta") final_rotors.remove("Gamma") for i, model in enumerate(final_rotors): radios = QRadioButton(model, rotor_frame) button_group.addButton(radios) button_group.setId(radios, i) rotor_layout.addWidget(radios, alignment=Qt.AlignTop) button_group.button(0).setChecked(True) # RINGSTELLUNG ===================================================== combobox = QComboBox(rotor_frame) for i, label in enumerate(LABELS[:len(charset)]): combobox.addItem(label, i) h_rule = QFrame(rotor_frame) h_rule.setFrameShape(QFrame.HLine) h_rule.setFrameShadow(QFrame.Sunken) self.__ring_selectors.append(combobox) self.__rotor_selectors.append(button_group) rotor_layout.addStretch() rotor_layout.addWidget(h_rule) rotor_layout.addWidget( QLabel("RING SETTING", rotor_frame, styleSheet=style), alignment=Qt.AlignHCenter, ) rotor_layout.addWidget(combobox) self.__settings_layout.addWidget(rotor_frame) self.__rotor_frames.append(rotor_frame) def clear_components(self): """Deletes all components settings widgets""" while True: child = self.__settings_layout.takeAt(0) if not child: break wgt = child.widget() wgt.deleteLater() del wgt def regenerate_for_model(self, new_model): """Regenerates component settings :param new_model: {str} Enigma model """ logging.info("Regenerating component settings...") self.clear_components() reflectors = self.__enigma_api.model_labels(new_model)["reflectors"] rotors = self.__enigma_api.model_labels(new_model)["rotors"] rotor_n = self.__enigma_api.rotor_n(new_model) charset = HISTORICAL[new_model]["charset"] self.generate_components(reflectors, rotors[::], rotor_n, charset) defaults = self.__enigma_api.default_cfg(new_model, rotor_n)[1] for selected, i in zip(defaults, range(rotor_n)): self.__rotor_selectors[i].button(selected).setChecked(True) self.__ukwd_window.clear_pairs() self.__ukwd_window._old_pairs = {} if new_model == self.__enigma_api.model(): self.load_from_api() self.__ukwd_window.refresh_pairs() self.refresh_ukwd() def load_from_api(self): """Loads displayed settings from shared EnigmaAPI instance""" logging.info("Loading component settings from EnigmaAPI...") model = self.__enigma_api.model() reflectors = self.__enigma_api.model_labels(model)["reflectors"] rotors = self.__enigma_api.model_labels(model)["rotors"] if "Beta" in rotors: rotors.remove("Beta") rotors.remove("Gamma") reflector_i = reflectors.index(self.__enigma_api.reflector()) self.__reflector_group.button(reflector_i).setChecked(True) for i, rotor in enumerate(self.__enigma_api.rotors()): if (model == "Enigma M4" and self.__enigma_api.reflector() != "UKW-D" and i == 0): rotor_i = ["Beta", "Gamma"].index(rotor) else: rotor_i = rotors.index(rotor) self.__rotor_selectors[i].button(rotor_i).setChecked(True) for i, ring in enumerate(self.__enigma_api.ring_settings()): self.__ring_selectors[i].setCurrentIndex(ring - 1) def collect(self): """Collects all selected settings for rotors and other components, applies them to the EnigmaAPI as new settings""" logging.info("Collecting new settings...") new_model = self.__stacked_wikis.currently_selected new_reflector = self.__reflector_group.checkedButton().text( ) # REFLECTOR CHOICES reflector_pairs = self.__ukwd_window.pairs() if new_reflector == "UKW-D" and new_model == "Enigma M4": new_rotors = [ group.checkedButton().text() for group in self.__rotor_selectors[1:] ] else: new_rotors = [ group.checkedButton().text() for group in self.__rotor_selectors ] ring_settings = [ ring.currentIndex() + 1 for ring in self.__ring_selectors ] logging.info("EnigmaAPI state before applying settings:\n%s", str(self.__enigma_api)) if new_model != self.__enigma_api.model(): self.__enigma_api.model(new_model) if new_reflector != self.__enigma_api.reflector(): self.__enigma_api.reflector(new_reflector) if new_reflector == "UKW-D": self.__enigma_api.reflector_pairs(reflector_pairs) if new_rotors != self.__enigma_api.rotors(): self.__enigma_api.rotors(new_rotors) if ring_settings != self.__enigma_api.ring_settings(): self.__enigma_api.ring_settings(ring_settings) logging.info("EnigmaAPI state when closing settings:\n%s", str(self.__enigma_api)) self.close() def pairs(self): """Returns current UKW-D pairs for collection""" return self._pairs
class App(QWidget): def __init__(self, bk, prefs): super().__init__() self.bk = bk self.prefs = prefs self.update = False # Install translator for the DOCXImport plugin dialog. # Use the Sigil language setting unless manually overridden. plugin_translator = QTranslator() if prefs['language_override'] is not None: print('Plugin preferences language override in effect') qmf = '{}_{}'.format(bk._w.plugin_name.lower(), prefs['language_override']) else: qmf = '{}_{}'.format(bk._w.plugin_name.lower(), bk.sigil_ui_lang) print( qmf, os.path.join(bk._w.plugin_dir, bk._w.plugin_name, 'translations')) plugin_translator.load( qmf, os.path.join(bk._w.plugin_dir, bk._w.plugin_name, 'translations')) print(QCoreApplication.instance().installTranslator(plugin_translator)) self._ok_to_close = False self.FTYPE_MAP = { 'smap': { 'title': _translate('App', 'Select custom style-map file'), 'defaultextension': '.txt', 'filetypes': 'Text Files (*.txt);;All files (*.*)', }, 'css': { 'title': _translate('App', 'Select custom CSS file'), 'defaultextension': '.css', 'filetypes': 'CSS Files (*.css)', }, 'docx': { 'title': _translate('App', 'Select DOCX file'), 'defaultextension': '.docx', 'filetypes': 'DOCX Files (*.docx)', }, } # Check online github files for newer version if self.prefs['check_for_updates']: self.update, self.newversion = self.check_for_update() self.initUI() def initUI(self): main_layout = QVBoxLayout(self) self.setWindowTitle('DOCXImport') self.upd_layout = QVBoxLayout() self.update_label = QLabel() self.update_label.setAlignment(Qt.AlignCenter) self.upd_layout.addWidget(self.update_label) self.get_update_button = QPushButton() self.get_update_button.clicked.connect(self.get_update) self.upd_layout.addWidget(self.get_update_button) main_layout.addLayout(self.upd_layout) if not self.update: self.update_label.hide() self.get_update_button.hide() self.details_grid = QGridLayout() self.epub2_select = QRadioButton() self.epub2_select.setText('EPUB2') self.epubType = QButtonGroup() self.epubType.addButton(self.epub2_select) self.details_grid.addWidget(self.epub2_select, 0, 0, 1, 1) self.checkbox_get_updates = QCheckBox() self.details_grid.addWidget(self.checkbox_get_updates, 0, 1, 1, 1) self.epub3_select = QRadioButton() self.epub3_select.setText('EPUB3') self.epubType.addButton(self.epub3_select) self.details_grid.addWidget(self.epub3_select, 1, 0, 1, 1) main_layout.addLayout(self.details_grid) self.checkbox_get_updates.setChecked(self.prefs['check_for_updates']) if self.prefs['epub_version'] == '2.0': self.epub2_select.setChecked(True) elif self.prefs['epub_version'] == '3.0': self.epub3_select.setChecked(True) else: self.epub2_select.setChecked(True) self.groupBox = QGroupBox() self.groupBox.setTitle('') self.verticalLayout_2 = QVBoxLayout(self.groupBox) self.docx_grid = QGridLayout() self.docx_label = QLabel() self.docx_grid.addWidget(self.docx_label, 0, 0, 1, 1) self.docx_path = QLineEdit() self.docx_grid.addWidget(self.docx_path, 1, 0, 1, 1) self.choose_docx_button = QPushButton() self.choose_docx_button.setText('...') self.docx_grid.addWidget(self.choose_docx_button, 1, 1, 1, 1) self.verticalLayout_2.addLayout(self.docx_grid) self.choose_docx_button.clicked.connect( lambda: self.fileChooser('docx', self.docx_path)) if len(self.prefs['lastDocxPath']): self.docx_path.setText(self.prefs['lastDocxPath']) self.docx_path.setEnabled(False) self.smap_grid = QGridLayout() self.checkbox_smap = QCheckBox(self.groupBox) self.smap_grid.addWidget(self.checkbox_smap, 0, 0, 1, 1) self.cust_smap_path = QLineEdit(self.groupBox) self.smap_grid.addWidget(self.cust_smap_path, 1, 0, 1, 1) self.choose_smap_button = QPushButton(self.groupBox) self.choose_smap_button.setText('...') self.smap_grid.addWidget(self.choose_smap_button, 1, 1, 1, 1) self.verticalLayout_2.addLayout(self.smap_grid) self.checkbox_smap.setChecked(self.prefs['useSmap']) self.checkbox_smap.stateChanged.connect(lambda: self.chkBoxActions( self.checkbox_smap, self.choose_smap_button)) self.choose_smap_button.clicked.connect( lambda: self.fileChooser('smap', self.cust_smap_path, self. checkbox_smap, self.choose_smap_button)) if len(self.prefs['useSmapPath']): self.cust_smap_path.setText(self.prefs['useSmapPath']) self.cust_smap_path.setEnabled(False) self.chkBoxActions(self.checkbox_smap, self.choose_smap_button) self.css_grid = QGridLayout() self.checkbox_css = QCheckBox(self.groupBox) self.css_grid.addWidget(self.checkbox_css, 0, 0, 1, 1) self.cust_css_path = QLineEdit(self.groupBox) self.css_grid.addWidget(self.cust_css_path, 1, 0, 1, 1) self.choose_css_button = QPushButton(self.groupBox) self.choose_css_button.setText('...') self.css_grid.addWidget(self.choose_css_button, 1, 1, 1, 1) self.verticalLayout_2.addLayout(self.css_grid) self.checkbox_css.setChecked(self.prefs['useCss']) self.checkbox_css.stateChanged.connect(lambda: self.chkBoxActions( self.checkbox_css, self.choose_css_button)) self.choose_css_button.clicked.connect( lambda: self.fileChooser('css', self.cust_css_path, self. checkbox_css, self.choose_css_button)) if len(self.prefs['useCssPath']): self.cust_css_path.setText(self.prefs['useCssPath']) self.cust_css_path.setEnabled(False) self.chkBoxActions(self.checkbox_css, self.choose_css_button) main_layout.addWidget(self.groupBox) self.checkbox_debug = QCheckBox() main_layout.addWidget(self.checkbox_debug) self.checkbox_debug.setChecked(self.prefs['debug']) spacerItem = QSpacerItem(20, 15, QSizePolicy.Minimum, QSizePolicy.Expanding) main_layout.addItem(spacerItem) button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(self._ok_clicked) button_box.rejected.connect(self._cancel_clicked) main_layout.addWidget(button_box) self.retranslateUi(self) if self.prefs['qt_geometry'] is not None: try: self.restoreGeometry( QByteArray.fromHex( self.prefs['qt_geometry'].encode('ascii'))) except: pass self.show() def retranslateUi(self, App): self.update_label.setText(_translate('App', 'Plugin Update Available')) self.get_update_button.setText(_translate('App', 'Go to download page')) self.checkbox_get_updates.setText( _translate('App', 'Check for plugin updates')) self.docx_label.setText(_translate('App', 'DOCX File to import')) self.checkbox_smap.setText(_translate('App', 'Use Custom Style Map')) self.checkbox_css.setText(_translate('App', 'Use Custom CSS')) self.checkbox_debug.setText( _translate('App', 'Debug Mode (change takes effect next plugin run)')) def fileChooser(self, ftype, qlineedit, qcheck=None, qbutton=None): options = QFileDialog.Options() options |= QFileDialog.DontUseNativeDialog title = self.FTYPE_MAP[ftype]['title'] startfolder = self.prefs['lastDir'][ftype] ffilter = self.FTYPE_MAP[ftype]['filetypes'] inpath, _ = QFileDialog.getOpenFileName(self, title, startfolder, ffilter, options=options) if len(inpath): qlineedit.setEnabled(True) qlineedit.setText(os.path.normpath(inpath)) self.prefs['lastDir'][ftype] = os.path.dirname(inpath) qlineedit.setEnabled(False) else: if qcheck is not None: qcheck.setChecked(False) if qbutton is not None: qbutton.setEnabled(False) def chkBoxActions(self, chk, btn): btn.setEnabled(chk.isChecked()) def cmdDo(self): global _DETAILS self.prefs['qt_geometry'] = self.saveGeometry().toHex().data().decode( 'ascii') self.prefs['check_for_updates'] = self.checkbox_get_updates.isChecked() self.prefs['epub_version'] = self.epubType.checkedButton().text( )[-1] + '.0' self.prefs['debug'] = self.checkbox_debug.isChecked() _DETAILS['vers'] = self.epubType.checkedButton().text()[-1] + '.0' self.prefs['useSmap'] = self.checkbox_smap.isChecked() if self.checkbox_smap.isChecked(): if len(self.cust_smap_path.text()): self.prefs['useSmapPath'] = self.cust_smap_path.text() _DETAILS['smap'] = (self.checkbox_smap.isChecked(), self.cust_smap_path.text()) else: # Message box that no file is selected return self.prefs['useCss'] = self.checkbox_css.isChecked() if self.checkbox_css.isChecked(): if len(self.cust_css_path.text()): self.prefs['useCssPath'] = self.cust_css_path.text() _DETAILS['css'] = (self.checkbox_css.isChecked(), self.cust_css_path.text()) else: # Message box that no file is selected return if len(self.docx_path.text()): self.prefs['lastDocxPath'] = self.docx_path.text() _DETAILS['docx'] = self.docx_path.text() else: # Message box that no file is selected return def check_for_update(self): '''Use updatecheck.py to check for newer versions of the plugin''' chk = UpdateChecker(self.prefs['last_time_checked'], self.bk._w) update_available, online_version, time = chk.update_info() # update preferences with latest date/time/version self.prefs['last_time_checked'] = time if online_version is not None: self.prefs['last_online_version'] = online_version if update_available: return (True, online_version) return (False, online_version) def get_update(self): url = DOWNLOAD_PAGE if self.update: latest = '/tag/v{}'.format(self.newversion) url = url + latest webbrowser.open_new_tab(url) def _ok_clicked(self): self._ok_to_close = True self.cmdDo() self.bk.savePrefs(self.prefs) QCoreApplication.instance().quit() def _cancel_clicked(self): self._ok_to_close = True '''Close aborting any changes''' self.prefs['qt_geometry'] = self.saveGeometry().toHex().data().decode( 'ascii') self.prefs['check_for_updates'] = self.checkbox_get_updates.isChecked() self.prefs['debug'] = self.checkbox_debug.isChecked() self.bk.savePrefs(self.prefs) QCoreApplication.instance().quit() def closeEvent(self, event): if self._ok_to_close: event.accept() # let the window close else: self._cancel_clicked()
class AssetBrowser(QDialog, Ui_AssetBrowser): mongo_host = mongo['hostname'] mongo_port = mongo['port'] mongo_db = 'tag_model' selected_tags = [] job = {} jobs = [] job_name = None stage_name = None entity_name = None asset_tags = {} asset_grps = {} stages = ['build', 'rnd', 'previs', 'shots'] entities = [] def __init__(self, current_path=None): super(self.__class__, self).__init__() self.setupUi(self) # Create button group self.grp_buttons = QButtonGroup() self.btn_import.released.connect(self.import_asset) self.initDB() self.getJobs() self.parseJobPath(current_path) self.initJob() self.populateStages() self.populateEntities() # Pre-set combo boxes self.cmb_stages.setCurrentIndex(self.stages.index(self.stage_name)) self.cmb_jobs.setCurrentIndex(self.jobs.index(self.job_name)) self.cmb_entities.setCurrentIndex(self.entities.index( self.entity_name)) self.le_in.tagSelected.connect(self.activate_tag) self.cmb_jobs.currentIndexChanged.connect(self.change_job) self.cmb_stages.currentIndexChanged.connect(self.change_stage) self.cmb_entities.currentIndexChanged.connect(self.change_entity) self.refresh_asset_list() # Set stylesheet self.setStyleSheet(hou.qt.styleSheet()) self.show() def change_entity(self): self.entity = self.cmb_entities.currentText() self.refresh_asset_list() # # Init database object # def initDB(self): client = pymongo.MongoClient(self.mongo_host, self.mongo_port) self.db = client[self.mongo_db] self.setWindowTitle('{} [{}:{}]'.format(self.windowTitle(), self.mongo_host, self.mongo_port)) # Get list of jobs def getJobs(self): jobs = self.db.jobs.find({}) for job in jobs: self.jobs.append(job['name']) self.cmb_jobs.addItem(job['name']) def populateStages(self): for stage in self.stages: self.cmb_stages.addItem(stage) # Validate current_path def parseJobPath(self, current_path): try: job_path = current_path.split('/') vfx_index = job_path.index('vfx') self.job_name = job_path[vfx_index - 1] # Try to set stage, but if we are not deep enough, default to build try: self.stage_name = job_path[vfx_index + 1] except IndexError: self.stage_name = 'shots' # Same with entity try: self.entity_name = job_path[vfx_index + 2] except IndexError: self.entity_name = 'sh0001' except: print "VFX Directory not found. No valid jobs detected" self.stage_name = 'shots' self.entity_name = 'sh0001' def change_job(self, i): self.job_name = self.cmb_jobs.currentText() self.initJob() self.populateEntities() self.refresh_asset_list() def change_stage(self, i): self.stage_name = self.cmb_stages.currentText() # Trigger rebuilding of entity combo box list self.populateEntities() self.refresh_asset_list() def populateEntities(self): self.cmb_entities.clear() self.entities = [] entity_root = '{}/vfx/{}'.format(self.job['path'], self.stage_name) entities = [x for x in os.listdir(entity_root) if x[0] is not '_'] for entity in entities: self.entities.append(entity) self.cmb_entities.addItem(entity) # # Initialize job variables and derive JobID from job path as well as # asset details attached to job # def initJob(self): # Reset self.asset_grps = {} self.asset_tags = {} self.selected_tags = [] # Pick up job_name if self.job_name is not None: self.job = self.db['jobs'].find_one({"name": self.job_name}) else: self.job = self.db['jobs'].find_one({}) self.job_name = self.job['name'] # Get all assets matching the job_id self.assets = [ x for x in self.db.assets_curr.find({"job_id": self.job['_id']}) ] # Resolve tag_ids for i, asset in enumerate(self.assets): if 'tags' in self.assets[i].keys(): self.assets[i]['tags'] = [ self.job['pooled_asset_tags'][x] for x in asset['tags'] ] # Consolidate asset entries by name for asset in self.assets: if asset['name'] not in self.asset_grps: self.asset_grps[asset['name']] = [] self.asset_grps[asset['name']].append(asset) # Bring tags to top level of dict for k, grp in self.asset_grps.iteritems(): flt = [x for x in grp if x.get('tags') is not None] self.asset_tags[k] = { tag for sublist in [x.get('tags') for x in flt] for tag in sublist } self.le_in.set_list(self.job['pooled_asset_tags']) self.rebuild_tags() def rebuild_tags(self): for i in range(0, self.ly_in.count() - 1): self.ly_in.itemAt(i).widget().close() # Repopulate tag list for j, tag in enumerate(self.selected_tags): tl = TagLabel(tag) tl.tagClicked.connect(self.remove_tag) self.ly_in.insertWidget(j, tl) # # When completion is clicked, add tag to selected tags list and redraw # tags in QHBoxLayout # def activate_tag(self, text): if text not in self.selected_tags: # Add to list of selected tags and update completion model self.selected_tags.append(text) self.rebuild_tags() self.le_in.set_list([ x for x in self.le_in.model.stringList() if x not in self.selected_tags ]) self.refresh_asset_list() def remove_tag(self, txt): self.selected_tags.remove(txt) self.rebuild_tags() current_model = self.le_in.model.stringList() current_model.append(txt) self.le_in.set_list(current_model) self.refresh_asset_list() def refresh_asset_list(self): types = { 'model': 1, 'layout': 2, 'anim': 3, 'fx_prep': 4, 'fx_sim': 5, 'shader': 6 } # Match tags matched_assets = [ k for k, v in self.asset_tags.iteritems() if all(y in v for y in self.selected_tags) ] # Filter: all models from build # Clear table while self.tbl_assets.rowCount() > 0: self.tbl_assets.removeRow(0) # Filter assets based on our rules: # 1. Include all build+models+shaders # 2. Include all build+shaders # 3. Filter everything else based on stage/entity filtered_assets = {} for key, grp in self.asset_grps.iteritems(): filtered_assets[key] = [] if key in matched_assets: for asset in grp: if asset['stage'] == 'build' and asset['type'] == 'model': filtered_assets[key].append(asset) elif asset['stage'] == 'build' and asset[ 'type'] == 'shader': filtered_assets[key].append(asset) elif asset['stage'] == self.cmb_stages.currentText( ) and asset['entity'] == self.cmb_entities.currentText(): filtered_assets[key].append(asset) for asset_name in matched_assets: self.tbl_assets.insertRow(0) item = QTableWidgetItem(asset_name) item.setFlags(QtCore.Qt.ItemIsEnabled) self.tbl_assets.setItem(0, 0, item) asset = filtered_assets[asset_name] for sub_asset in asset: # Safely grab column index based on asset type. Default # to 1 if no type is present. tbl_pos = types.get(sub_asset.get('type')) if tbl_pos is None: tbl_pos = 1 rad = AssetCell('v{}'.format(str(sub_asset['version'])), sub_asset['filepath'], sub_asset['_id']) self.grp_buttons.addButton(rad.getRad()) self.tbl_assets.setCellWidget(0, tbl_pos, rad) def import_asset(self): # Get currently selected AssetCell in QButtonGroup print self.grp_buttons.checkedButton().parent().getFilepath() obj_id = self.grp_buttons.checkedButton().parent().getObjId() importHoudiniAsset(self.db, obj_id)
class ParserGUI(QWidget): def __init__(self, format_config): """The main GUI of the ref parser The class take output formats as an input """ super().__init__() self.resize(800, 600) self.setWindowTitle("RefParse") self.init_layout(format_config) self.format_config = format_config self.api_object = None self.__threads = [] def init_layout(self, format_config): """Initiate layouts For simplicity, the widget uses gridlayout with 2 columns """ grid = QGridLayout() # - reference line self.ref_line = QLineEdit(self) grid.addWidget(QLabel("doi/arXiv:"), 0, 0) grid.addWidget(self.ref_line, 0, 1) # - search button self.search_btn = QPushButton("Search") self.search_btn.setShortcut("Return") self.search_btn.clicked.connect(self.access_reference) search = QHBoxLayout() search.addStretch(1) search.addWidget(self.search_btn) grid.addLayout(search, 1, 1) # - Output grid.addWidget(QLabel("Output:"), 2, 0) # - output format, set the first button to be true format_opt = QVBoxLayout() self.format_btns = QButtonGroup(self) self.format_btns.buttonClicked.connect(self.change_format) for format_type in format_config.keys(): btn = QRadioButton(format_type, self) format_opt.addWidget(btn) self.format_btns.addButton(btn) # set first button checked self.format_btns.button(-2).setChecked(True) self.output_box = QTextEdit(self) grid.addLayout(format_opt, 3, 0, Qt.AlignTop) grid.addWidget(self.output_box, 3, 1) # - copy button self.copy_btn = QPushButton("Copy") self.copy_btn.clicked.connect(self.copy) copy = QHBoxLayout() copy.addStretch(1) copy.addWidget(self.copy_btn) grid.addLayout(copy, 4, 1) # - log box (use custom logging handler that stream log to text box) self.log_box = QTextEdit(self) self.log_box.setReadOnly(True) self.log_box.setStyleSheet("background-color: transparent;") # set the log box to half of the size self.log_box.setMaximumHeight(self.log_box.sizeHint().height() / 2) # custom signal handler log_handler = QLogHandler(self) log_handler.setFormatter( logging.Formatter("[%(levelname)s] %(name)s - %(message)s")) root_logger.addHandler(log_handler) log_handler.signal.log_str.connect(self.log_box.append) grid.addWidget(QLabel("Log:"), 5, 0, Qt.AlignTop) grid.addWidget(self.log_box, 5, 1) # setup the overall layout for the widget self.setLayout(grid) # debug shortcuts self.debug = QShortcut(QKeySequence("Shift+F8"), self) self.debug.activated.connect(self.toggle_debug) def access_reference(self): """Create thread to run the api When the search button is pressed, the contents is reset and a new thread is created to run the api object """ self.reset_content() reference = self.ref_line.text() if reference: gui_logger.info(f"Search reference: {reference}") ref_thread = RefThread(reference, self.format_config, parent=self) ref_thread.response_obj.connect(self.output) ref_thread.start() else: gui_logger.warning(f"Please enter doi or arXiv ID") @Slot(object) def output(self, api_object): """Link the api object to GUI class The slot for the signal from access_reference store the api object to the GUI """ self.api_object = api_object self.change_format() def change_format(self): """Test the group of button if it is checked""" ref_format = self.format_btns.checkedButton().text() self.output_box.clear() if self.api_object.status: gui_logger.debug(f"{ref_format} format") format_thread = FormatThread(self.api_object, ref_format, parent=self) format_thread.response_str.connect(self.update_output) format_thread.start() @Slot(str) def update_output(self, output_str): """Update output from stored parsed api object This is done by first checking which format button is clicked """ self.output_box.setText(output_str) def reset_content(self): """Clear and reset the contents""" self.output_box.clear() self.log_box.clear() def copy(self): """Copy the output text""" clipboard = QApplication.clipboard() clipboard.setText(self.output_box.toPlainText()) gui_logger.info("text copied to clipboard") def toggle_debug(self): """Secret short cut shift + F8 to switch to debug mode""" if root_logger.isEnabledFor(logging.DEBUG): gui_logger.info("switch to regular mode") root_logger.setLevel(logging.INFO) else: gui_logger.info("switch to debug mode") root_logger.setLevel(logging.DEBUG) def closeEvent(self, event): """Exit and wait for the thread to finish""" for thread in self.__threads: thread[0].quit() thread[0].wait()