Пример #1
0
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
Пример #2
0
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
Пример #3
0
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
Пример #4
0
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()
Пример #5
0
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)
Пример #6
0
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()