class SuccessPage(QWizardPage): def __init__(self, parent): super().__init__(parent) self.setTitle("Installation successful") self._label = HyperTextLabel() layout = QVBoxLayout(self) use_julia_check_box = QCheckBox("Use this Julia with Spine Toolbox") self._create_kernel_check_box = QCheckBox( "Create a Jupyter kernel for this Julia") self.registerField("use_julia", use_julia_check_box) self.registerField("create_kernel", self._create_kernel_check_box) layout.addWidget(self._label) layout.addStretch() layout.addWidget(use_julia_check_box) layout.addWidget(self._create_kernel_check_box) layout.addStretch() layout.addStretch() use_julia_check_box.clicked.connect(self._handle_use_julia_clicked) @Slot(bool) def _handle_use_julia_clicked(self, checked=False): self._create_kernel_check_box.setChecked(checked) self._create_kernel_check_box.setEnabled(checked) def initializePage(self): self._label.setText( f"Julia executable created at <b>{self.wizard().julia_exe}</b>") self.setField("use_julia", True) self.setField("create_kernel", True) def nextId(self): return -1
def paint(self, painter, option, index): checked = bool(index.data()) checkbox = QCheckBox() if (index.flags() & Qt.ItemIsEditable) > 0: checkbox.setEnabled(True) else: checkbox.setEnabled(False) # Implement tristate checkboxe for folder nodes if checked: checkbox.setCheckState(Qt.Checked) else: checkbox.setCheckState(Qt.Unchecked) if self.parent(): checkbox.setStyleSheet(self.parent().styleSheet()) width = option.widget.columnWidth(1) height = option.widget.rowHeight(0) painter.save() painter.translate(option.rect.topLeft()) checkbox.rect = option.rect checkbox.setFixedSize(width, height) checkbox.render(painter, QPoint(0, 0)) painter.restore()
class CheckBoxAction(QWidget): def __init__(self, parent=None, text: str = "", checked: bool = False, onState: str = "Enabled", offState: str = "Disabled"): super().__init__(parent=parent) self.onState = onState self.offState = offState self.setLayout(QtWidgets.QHBoxLayout(self)) self.avoidInternalChecking = False self.label = QLabel(text) self.layout().addWidget(self.label) self.layout().setMargin(1) self.check = QCheckBox(self) self.layout().addWidget(self.check) self.check.setChecked(checked) self.check.setObjectName("QCheckBoxAction") self.check.stateChanged.connect(self.changeText) self.changeText() def winIsLight(self) -> bool: mode = darkdetect.isLight() if (mode != None): return mode else: return True def setText(self, text: str) -> None: self.label.setText(text) def setEnabled(self, enabled: bool) -> None: self.check.setEnabled(enabled) self.changeText() def isChecked(self) -> bool: return self.check.isChecked() def setCheckedWithoutInternalChecking(self, value: bool) -> None: self.avoidInternalChecking = True return self.check.setChecked(value) def setChecked(self, value: bool) -> None: return self.check.setChecked(value) def changeText(self) -> None: if (self.check.isChecked()): self.check.setText(self.onState) else: self.check.setText(self.offState)
def paint(self, painter, option, index): checked = bool(index.data()) checkbox = QCheckBox() if (index.flags() & Qt.ItemIsEditable) > 0: checkbox.setEnabled(True) else: checkbox.setEnabled(False) if checked: checkbox.setCheckState(Qt.Checked) else: checkbox.setCheckState(Qt.Unchecked) checkbox.rect = self.getCheckBoxRect(option) #checkbox.setCheckState(QStyle.State_Enabled) style = '''QCheckBox, QRadioButton { color: #546E7A; } QCheckBox::indicator::unchecked { background-color: #FFFFFF; border: 1px solid #536D79; } QCheckBox::indicator::checked, QTreeView::indicator::checked { background-color: qradialgradient(cx:0.5, cy:0.5, fx:0.25, fy:0.15, radius:0.3, stop:0 #80CBC4, stop:1 #FFFFFF); border: 1px solid #536D79; } QCheckBox::indicator:disabled, QRadioButton::indicator:disabled, QTreeView::indicator:disabled { background-color: #444444; /* Not sure what this looks like */ } QCheckBox::indicator::checked:disabled, QRadioButton::indicator::checked:disabled, QTreeView::indicator::checked:disabled { background-color: qradialgradient(cx:0.5, cy:0.5, fx:0.25, fy:0.15, radius:0.3, stop:0 #BBBBBB, stop:1 #444444); /* Not sure what this looks like */ } ''' checkbox.setStyleSheet(style) painter.save() painter.translate(option.rect.topLeft()) checkbox.render(painter, QPoint(0, 0)) painter.restore()
def add_horizontal_splitter(self, layout): splitter = QSplitter() splitter.setOrientation(Qt.Orientation.Vertical) layout.addWidget(splitter) # ADD TOP top_widget = QWidget(splitter) top = QVBoxLayout(top_widget) # NORMAL btn = FillPushButton('Push Button') top.layout().addWidget(btn) #DISABLED btn = FillPushButton('Push Button Disabled') btn.setEnabled(False) top.layout().addWidget(btn) # ADD BOTTOM bottom_widget = QWidget(splitter) right = QGridLayout(bottom_widget) # NORMAL cbx = QCheckBox('Check Box') right.addWidget(cbx) #DISABLED cbx = QCheckBox('Check Box Disabled') cbx.setEnabled(False) right.addWidget(cbx) rob = QRadioButton('Radio Button') right.addWidget(rob) rob = QRadioButton('Radio Button Disabled') rob.setEnabled(False) right.addWidget(rob)
def WidgetBlockOnCheck(Activation: QtWidgets.QCheckBox, activate: bool): # print(Activation.__class__.__name__) if Activation.__class__.__name__ == "QCheckBox": if Activation.isChecked() == activate: Activation.setChecked(activate) Activation.setEnabled(activate) else: Activation.setChecked(False) Activation.setEnabled(activate) elif Activation.__class__.__name__ == "QPushButton": Activation.setEnabled(activate)
class ColorDialog(QDialog): def __init__(self, model, font_metric, parent=None): super().__init__(parent) self.setWindowTitle('Color Options') self.model = model self.font_metric = font_metric self.main_window = parent self.createDialogLayout() def createDialogLayout(self): self.createGeneralTab() self.cellTable = self.createDomainTable(self.main_window.cellsModel) self.matTable = self.createDomainTable(self.main_window.materialsModel) self.tabs = { 'cell': self.createDomainTab(self.cellTable), 'material': self.createDomainTab(self.matTable), 'temperature': self.createPropertyTab('temperature'), 'density': self.createPropertyTab('density') } self.tab_bar = QTabWidget() self.tab_bar.setMaximumHeight(800) self.tab_bar.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.tab_bar.addTab(self.generalTab, 'General') self.tab_bar.addTab(self.tabs['cell'], 'Cells') self.tab_bar.addTab(self.tabs['material'], 'Materials') self.tab_bar.addTab(self.tabs['temperature'], 'Temperature') self.tab_bar.addTab(self.tabs['density'], 'Density') self.createButtonBox() self.colorDialogLayout = QVBoxLayout() self.colorDialogLayout.addWidget(self.tab_bar) self.colorDialogLayout.addWidget(self.buttonBox) self.setLayout(self.colorDialogLayout) def createGeneralTab(self): main_window = self.main_window # Masking options self.maskingCheck = QCheckBox('') self.maskingCheck.stateChanged.connect(main_window.toggleMasking) button_width = self.font_metric.boundingRect("XXXXXXXXXX").width() self.maskColorButton = QPushButton() self.maskColorButton.setCursor(QtCore.Qt.PointingHandCursor) self.maskColorButton.setFixedWidth(button_width) self.maskColorButton.setFixedHeight(self.font_metric.height() * 1.5) self.maskColorButton.clicked.connect(main_window.editMaskingColor) # Highlighting options self.hlCheck = QCheckBox('') self.hlCheck.stateChanged.connect(main_window.toggleHighlighting) self.hlColorButton = QPushButton() self.hlColorButton.setCursor(QtCore.Qt.PointingHandCursor) self.hlColorButton.setFixedWidth(button_width) self.hlColorButton.setFixedHeight(self.font_metric.height() * 1.5) self.hlColorButton.clicked.connect(main_window.editHighlightColor) self.alphaBox = QDoubleSpinBox() self.alphaBox.setRange(0, 1) self.alphaBox.setSingleStep(.05) self.alphaBox.valueChanged.connect(main_window.editAlpha) self.seedBox = QSpinBox() self.seedBox.setRange(1, 999) self.seedBox.valueChanged.connect(main_window.editSeed) # General options self.bgButton = QPushButton() self.bgButton.setCursor(QtCore.Qt.PointingHandCursor) self.bgButton.setFixedWidth(button_width) self.bgButton.setFixedHeight(self.font_metric.height() * 1.5) self.bgButton.clicked.connect(main_window.editBackgroundColor) self.colorbyBox = QComboBox(self) self.colorbyBox.addItem("material") self.colorbyBox.addItem("cell") self.colorbyBox.addItem("temperature") self.colorbyBox.addItem("density") self.colorbyBox.currentTextChanged[str].connect( main_window.editColorBy) self.universeLevelBox = QComboBox(self) self.universeLevelBox.addItem('all') for i in range(self.model.max_universe_levels): self.universeLevelBox.addItem(str(i)) self.universeLevelBox.currentTextChanged[str].connect( main_window.editUniverseLevel) # Overlap plotting self.overlapCheck = QCheckBox('', self) overlap_connector = partial(main_window.toggleOverlaps) self.overlapCheck.stateChanged.connect(overlap_connector) self.overlapColorButton = QPushButton() self.overlapColorButton.setCursor(QtCore.Qt.PointingHandCursor) self.overlapColorButton.setFixedWidth(button_width) self.overlapColorButton.setFixedHeight(self.font_metric.height() * 1.5) self.overlapColorButton.clicked.connect(main_window.editOverlapColor) self.colorResetButton = QPushButton("&Reset Colors") self.colorResetButton.setCursor(QtCore.Qt.PointingHandCursor) self.colorResetButton.clicked.connect(main_window.resetColors) formLayout = QFormLayout() formLayout.setAlignment(QtCore.Qt.AlignHCenter) formLayout.setFormAlignment(QtCore.Qt.AlignHCenter) formLayout.setLabelAlignment(QtCore.Qt.AlignLeft) formLayout.addRow('Masking:', self.maskingCheck) formLayout.addRow('Mask Color:', self.maskColorButton) formLayout.addRow(HorizontalLine()) formLayout.addRow('Highlighting:', self.hlCheck) formLayout.addRow('Highlight Color:', self.hlColorButton) formLayout.addRow('Highlight Alpha:', self.alphaBox) formLayout.addRow('Highlight Seed:', self.seedBox) formLayout.addRow(HorizontalLine()) formLayout.addRow('Background Color: ', self.bgButton) formLayout.addRow(HorizontalLine()) formLayout.addRow('Show Overlaps:', self.overlapCheck) formLayout.addRow('Overlap Color:', self.overlapColorButton) formLayout.addRow(HorizontalLine()) formLayout.addRow('Color Plot By:', self.colorbyBox) formLayout.addRow('Universe Level:', self.universeLevelBox) formLayout.addRow(self.colorResetButton, None) generalLayout = QHBoxLayout() innerWidget = QWidget() generalLayout.setAlignment(QtCore.Qt.AlignVCenter) innerWidget.setLayout(formLayout) generalLayout.addStretch(1) generalLayout.addWidget(innerWidget) generalLayout.addStretch(1) self.generalTab = QWidget() self.generalTab.setLayout(generalLayout) def createDomainTable(self, domainmodel): domainTable = QTableView() domainTable.setModel(domainmodel) domainTable.setItemDelegate(DomainDelegate(domainTable)) domainTable.verticalHeader().setVisible(False) domainTable.resizeColumnsToContents() domainTable.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) domainTable.horizontalHeader().setSectionResizeMode( 1, QHeaderView.Stretch) return domainTable def createDomainTab(self, domaintable): domainTab = QWidget() domainTab.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) domainLayout = QVBoxLayout() domainLayout.addWidget(domaintable) domainTab.setLayout(domainLayout) return domainTab def createPropertyTab(self, property_kind): propertyTab = QWidget() propertyTab.property_kind = property_kind propertyTab.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) propertyLayout = QVBoxLayout() propertyTab.minMaxCheckBox = QCheckBox() propertyTab.minMaxCheckBox.setCheckable(True) connector1 = partial(self.main_window.toggleUserMinMax, property=property_kind) propertyTab.minMaxCheckBox.stateChanged.connect(connector1) propertyTab.minBox = ScientificDoubleSpinBox(self) propertyTab.minBox.setMaximum(1E9) propertyTab.minBox.setMinimum(0) propertyTab.maxBox = ScientificDoubleSpinBox(self) propertyTab.maxBox.setMaximum(1E9) propertyTab.maxBox.setMinimum(0) connector2 = partial(self.main_window.editColorbarMin, property_type=property_kind) propertyTab.minBox.valueChanged.connect(connector2) connector3 = partial(self.main_window.editColorbarMax, property_type=property_kind) propertyTab.maxBox.valueChanged.connect(connector3) propertyTab.colormapBox = QComboBox(self) cmaps = sorted(m for m in mcolormaps.datad if not m.endswith("_r")) for cmap in cmaps: propertyTab.colormapBox.addItem(cmap) connector = partial(self.main_window.editColorMap, property_type=property_kind) propertyTab.colormapBox.currentTextChanged[str].connect(connector) propertyTab.dataIndicatorCheckBox = QCheckBox() propertyTab.dataIndicatorCheckBox.setCheckable(True) connector4 = partial(self.main_window.toggleDataIndicatorCheckBox, property=property_kind) propertyTab.dataIndicatorCheckBox.stateChanged.connect(connector4) propertyTab.colorBarScaleCheckBox = QCheckBox() propertyTab.colorBarScaleCheckBox.setCheckable(True) connector5 = partial(self.main_window.toggleColorbarScale, property=property_kind) propertyTab.colorBarScaleCheckBox.stateChanged.connect(connector5) formLayout = QFormLayout() formLayout.setAlignment(QtCore.Qt.AlignHCenter) formLayout.setFormAlignment(QtCore.Qt.AlignHCenter) formLayout.setLabelAlignment(QtCore.Qt.AlignLeft) formLayout.addRow('Colormap:', propertyTab.colormapBox) formLayout.addRow('Custom Min/Max', propertyTab.minMaxCheckBox) formLayout.addRow('Data Indicator', propertyTab.dataIndicatorCheckBox) formLayout.addRow('Log Scale', propertyTab.colorBarScaleCheckBox) formLayout.addRow(HorizontalLine()) formLayout.addRow('Max: ', propertyTab.maxBox) formLayout.addRow('Min: ', propertyTab.minBox) propertyTab.setLayout(formLayout) return propertyTab def updateDataIndicatorVisibility(self): av = self.model.activeView for key, val in av.data_indicator_enabled.items(): self.tabs[key].dataIndicatorCheckBox.setChecked(val) def updateColorMaps(self): cmaps = self.model.activeView.colormaps for key, val in cmaps.items(): idx = self.tabs[key].colormapBox.findText( val, QtCore.Qt.MatchFixedString) if idx >= 0: self.tabs[key].colormapBox.setCurrentIndex(idx) def updateColorMinMax(self): minmax = self.model.activeView.user_minmax for key, val in minmax.items(): self.tabs[key].minBox.setValue(val[0]) self.tabs[key].maxBox.setValue(val[1]) custom_minmax = self.model.activeView.use_custom_minmax for key, val, in custom_minmax.items(): self.tabs[key].minMaxCheckBox.setChecked(val) self.tabs[key].minBox.setEnabled(val) self.tabs[key].maxBox.setEnabled(val) def updateColorbarScale(self): av = self.model.activeView for key, val in av.color_scale_log.items(): self.tabs[key].colorBarScaleCheckBox.setChecked(val) def createButtonBox(self): applyButton = QPushButton("Apply Changes") applyButton.clicked.connect(self.main_window.applyChanges) closeButton = QPushButton("Close") closeButton.clicked.connect(self.hide) buttonLayout = QHBoxLayout() buttonLayout.addStretch(1) buttonLayout.addWidget(applyButton) buttonLayout.addWidget(closeButton) self.buttonBox = QWidget() self.buttonBox.setLayout(buttonLayout) def updateDialogValues(self): self.updateMasking() self.updateMaskingColor() self.updateColorMaps() self.updateColorMinMax() self.updateColorbarScale() self.updateDataIndicatorVisibility() self.updateHighlighting() self.updateHighlightColor() self.updateAlpha() self.updateSeed() self.updateBackgroundColor() self.updateColorBy() self.updateUniverseLevel() self.updateDomainTabs() self.updateOverlap() self.updateOverlapColor() def updateMasking(self): masking = self.model.activeView.masking self.maskingCheck.setChecked(masking) self.maskColorButton.setDisabled(not masking) if masking: self.cellTable.showColumn(4) self.matTable.showColumn(4) else: self.cellTable.hideColumn(4) self.matTable.hideColumn(4) def updateMaskingColor(self): color = self.model.activeView.maskBackground style_values = "border-radius: 8px; background-color: rgb{}" self.maskColorButton.setStyleSheet(style_values.format(str(color))) def updateHighlighting(self): highlighting = self.model.activeView.highlighting self.hlCheck.setChecked(highlighting) self.hlColorButton.setDisabled(not highlighting) self.alphaBox.setDisabled(not highlighting) self.seedBox.setDisabled(not highlighting) if highlighting: self.cellTable.showColumn(5) self.cellTable.hideColumn(2) self.cellTable.hideColumn(3) self.matTable.showColumn(5) self.matTable.hideColumn(2) self.matTable.hideColumn(3) else: self.cellTable.hideColumn(5) self.cellTable.showColumn(2) self.cellTable.showColumn(3) self.matTable.hideColumn(5) self.matTable.showColumn(2) self.matTable.showColumn(3) def updateHighlightColor(self): color = self.model.activeView.highlightBackground style_values = "border-radius: 8px; background-color: rgb{}" self.hlColorButton.setStyleSheet(style_values.format(str(color))) def updateAlpha(self): self.alphaBox.setValue(self.model.activeView.highlightAlpha) def updateSeed(self): self.seedBox.setValue(self.model.activeView.highlightSeed) def updateBackgroundColor(self): color = self.model.activeView.domainBackground self.bgButton.setStyleSheet("border-radius: 8px;" "background-color: rgb%s" % (str(color))) def updateOverlapColor(self): color = self.model.activeView.overlap_color self.overlapColorButton.setStyleSheet("border-radius: 8px;" "background-color: rgb%s" % (str(color))) def updateOverlap(self): colorby = self.model.activeView.colorby overlap_val = self.model.activeView.color_overlaps if colorby in ('cell', 'material'): self.overlapCheck.setChecked(overlap_val) def updateColorBy(self): colorby = self.model.activeView.colorby self.colorbyBox.setCurrentText(colorby) self.overlapCheck.setEnabled(colorby in ("cell", "material")) self.universeLevelBox.setEnabled(colorby == 'cell') def updateUniverseLevel(self): level = self.model.activeView.level if level == -1: self.universeLevelBox.setCurrentText('all') else: self.universeLevelBox.setCurrentText(str(level)) def updateDomainTabs(self): self.cellTable.setModel(self.main_window.cellsModel) self.matTable.setModel(self.main_window.materialsModel)
class IndexSche(QWidget): def __init__(self): super().__init__() self._init_ui() self._init_conn() def _init_ui(self): # self._btn_cron=QRadioButton(self.tr("Cron")) # self._btn_realtime=QRadioButton(self.tr("RealTime")) vlayout = QVBoxLayout() self.setLayout(vlayout) _group_cron = QGroupBox(self.tr("Cron")) vlayout.addWidget(_group_cron) _formLayout_Cron = QFormLayout() _group_cron.setLayout(_formLayout_Cron) _formLayout_Cron.addWidget(QLabel("xxxx")) self._label_cron_week = QLineEdit() self._label_cron_hour = QLineEdit() self._label_cron_minu = QLineEdit() _formLayout_Cron.addRow(self.tr("星期日"), self._label_cron_week) _formLayout_Cron.addRow(self.tr("小时"), self._label_cron_hour) _formLayout_Cron.addRow(self.tr("分钟"), self._label_cron_minu) self._btn_enable = QPushButton(self.tr("Enable")) self._btn_disable = QPushButton(self.tr("Disable")) # self._btn_cancel = QPushButton(self.tr("Cancel")) hlayout = QHBoxLayout() hlayout.addWidget(self._btn_enable) hlayout.addWidget(self._btn_disable) # hlayout.addWidget(self._btn_cancel) # self._btn_cron=QDialogButtonBox(QDialogButtonBox) _formLayout_Cron.addWidget(QLabel("ddd")) _formLayout_Cron.addRow(hlayout) _group_realtime = QGroupBox(self.tr("RealTime")) vlayout.addWidget(_group_realtime) formLayout = QFormLayout() _group_realtime.setLayout(formLayout) formLayout.addWidget(QLabel(self.tr("xxx"))) self._cbk_startOnStart = QCheckBox() self._cbk_startNow = QCheckBox() self._cbk_startNow.setEnabled(False) formLayout.addRow(self._cbk_startOnStart, QLabel(self.tr("Start on System Start"))) formLayout.addRow(self._cbk_startNow, QLabel(self.tr("Start Now"))) pass def _init_conn(self): self._btn_enable.clicked.connect(self._cron_enable) self._btn_disable.clicked.connect(self._cron_disable) self._cbk_startOnStart.toggled.connect(self._cbk_onstart_toggled) self._cbk_startNow.toggled.connect(self._cbk_start_now_toggled) pass def _cbk_onstart_toggled(self, checked): if checked: self._cbk_startNow.setEnabled(True) ## TODO imple pass def _cbk_start_now_toggled(self, checked): pass def _cron_enable(self): pass def _cron_disable(self): pass
class MainWidget(QWidget): format_dict = { "JPEG(jpg)": "jpeg", "WebP": "webp", "Gif": "gif", "PNG": "png" } def __init__(self, parent): super().__init__() self.par = parent self.InitUI() def InitUI(self): self.selected_dir = str() self.option_array = [1, 0, 0, 0] self.main_layout = QVBoxLayout() self.make_dir_groupbox() self.make_func_groupbox() self.make_sys_layout() self.status_bar = QStatusBar() self.setEnabled(True) self.setLayout(self.main_layout) def make_dir_groupbox(self): dir_groupbox = QGroupBox("Select Directory") dir_layout = QVBoxLayout() self.loaded_dir_label = QLabel(self.selected_dir) self.loaded_dir_label.setStyleSheet("max-height: 24px;" "background-color: #FFFFFF;" "border-style: solid;" "border-width: 1px;" "border-color: #000000") dir_layout.addWidget(self.loaded_dir_label) self.load_dir_button = QPushButton("Load Directory", self) self.load_dir_button.clicked.connect(self.load_dir) dir_layout.addWidget(self.load_dir_button) dir_groupbox.setLayout(dir_layout) self.main_layout.addWidget(dir_groupbox) def make_func_groupbox(self): func_layout = QHBoxLayout() self.encode_widget = RadioBox(encode_dict) encode_layout = self.encode_widget.make_radio_box("UTF-8 with BOM") self.encode_groupbox = QGroupBox("Encode to: ") self.encode_groupbox.setLayout(encode_layout) self.encode_groupbox.setCheckable(True) self.encode_groupbox.clicked.connect(self.option_set) func_layout.addWidget(self.encode_groupbox) self.fmt_groupbox = QGroupBox("Convert image format") fmt_hbox = QHBoxLayout() self.format_widget_from = RadioBox(self.format_dict) format_layout_from = self.format_widget_from.make_radio_box("WebP") fmt_from_groupbox = QGroupBox("Convert image from: ") fmt_from_groupbox.setLayout(format_layout_from) fmt_hbox.addWidget(fmt_from_groupbox) self.format_widget_to = RadioBox(self.format_dict) format_layout_to = self.format_widget_to.make_radio_box("PNG") fmt_to_groupbox = QGroupBox("Convert image to: ") fmt_to_groupbox.setLayout(format_layout_to) fmt_hbox.addWidget(fmt_to_groupbox) self.fmt_groupbox.setLayout(fmt_hbox) self.fmt_groupbox.setCheckable(True) func_layout.addWidget(self.fmt_groupbox) option_groupbox = QGroupBox("OPTION: ") option_layout = QVBoxLayout() self.change_txt_yn = QCheckBox("Change image ext in CSV/ERB", self) self.change_txt_yn.toggle() self.change_txt_yn.clicked.connect(self.option_set) self.backup_yn = QCheckBox("Place files in Result directory", self) self.backup_yn.clicked.connect(self.option_set) option_layout.addWidget(self.change_txt_yn) option_layout.addWidget(self.backup_yn) option_groupbox.setLayout(option_layout) func_layout.addWidget(option_groupbox) self.main_layout.addLayout(func_layout) def make_sys_layout(self): sys_layout = QHBoxLayout() self.run_button = QPushButton("RUN", self) self.run_button.setEnabled(False) self.run_button.clicked.connect(self.run_process) self.prog_bar = QProgressBar() self.prog_bar.setMinimum(0) sys_layout.addWidget(self.prog_bar) sys_layout.stretch(1) sys_layout.addWidget(self.run_button) self.main_layout.addLayout(sys_layout) def load_dir(self): f_dialog = QFileDialog(self, "불러올 폴더를 선택하세요.", os.path.curdir) f_dialog.setFileMode(QFileDialog.DirectoryOnly) self.selected_dir = f_dialog.getExistingDirectory( self, "불러올 폴더를 선택하세요.") if not self.selected_dir: self.selected_dir = str() self.run_button.setEnabled(False) else: self.run_button.setEnabled(True) self.loaded_dir_label.setText(self.selected_dir) return self.selected_dir def option_set(self): if not self.encode_groupbox.isChecked(): self.change_txt_yn.setEnabled(False) else: self.change_txt_yn.setEnabled(True) self.option_array[0] = self.change_txt_yn.checkState() self.option_array[1] = self.backup_yn.checkState() self.option_array[2] = self.encode_groupbox.isChecked() self.option_array[3] = self.fmt_groupbox.isChecked() return self.option_array def run_process(self): self.option_set() self.setEnabled(False) target_array = ( self.encode_widget.result, self.format_widget_from.result, self.format_widget_to.result, self.selected_dir, ) bar_array = (self.prog_bar, self.status_bar) self.threadclass = MyThread(target_array, bar_array, self.option_array, self) self.work_started = 0 self.threadclass.processed.connect(self.progbar_set) self.threadclass.finished.connect(self.donebox) self.threadclass.start() def open_result(self): if self.option_array[1]: result_path = "Result\\" else: result_path = self.selected_dir os.startfile(result_path) @Slot(int) def progbar_set(self, progress_stat): if not self.work_started: self.prog_bar.setMaximum(progress_stat) self.work_started += 1 else: self.prog_bar.setValue(progress_stat) @Slot(bool) def donebox(self): msgbox = QMessageBox( QMessageBox.Warning, "Done!", "Do you want to quit?", buttons=QMessageBox.Yes | QMessageBox.No, parent=self, ) open_result = msgbox.addButton("Open Result Folder", QMessageBox.AcceptRole) msgbox.setDefaultButton(QMessageBox.Yes) close_yn = msgbox.exec_() if close_yn == QMessageBox.No: self.threadclass.wait() # debugpy.debug_this_thread() self.close() self.par.intiGUI() else: if close_yn == QMessageBox.AcceptRole: self.open_result() QCoreApplication.instance().quit()
class MuxSettingTab(QWidget): tab_clicked_signal = Signal() start_muxing_signal = Signal() update_task_bar_progress_signal = Signal(int) update_task_bar_paused_signal = Signal() update_task_bar_clear_signal = Signal() def __init__(self): super().__init__() self.create_widgets() self.setup_widgets() self.connect_signals() def connect_signals(self): self.tab_clicked_signal.connect(self.tab_clicked) self.destination_path_button.clicked.connect(self.open_select_destination_folder_dialog) self.only_keep_those_audios_checkBox.stateChanged.connect( self.only_keep_those_audios_multi_choose_comboBox.check_box_state_changed) self.only_keep_those_subtitles_checkBox.stateChanged.connect( self.only_keep_those_subtitles_multi_choose_comboBox.check_box_state_changed) self.make_this_audio_default_checkBox.disable_combo_box.connect(self.disable_make_this_audio_default_comboBox) self.make_this_subtitle_default_checkBox.disable_combo_box.connect( self.disable_make_this_subtitle_default_comboBox) self.control_queue_button.add_to_queue_clicked_signal.connect(self.add_to_queue_button_clicked) self.control_queue_button.start_multiplexing_clicked_signal.connect(self.start_multiplexing_button_clicked) self.control_queue_button.pause_multiplexing_clicked_signal.connect(self.pause_multiplexing_button_clicked) self.clear_job_queue_button.clicked.connect(self.clear_job_queue_button_clicked) self.only_keep_those_audios_multi_choose_comboBox.closeList.connect(self.only_keep_those_audios_close_list) self.only_keep_those_subtitles_multi_choose_comboBox.closeList.connect( self.only_keep_those_subtitles_close_list) self.make_this_audio_default_comboBox.currentTextChanged.connect( self.make_this_audio_default_comboBox_text_changed) self.make_this_subtitle_default_comboBox.currentTextChanged.connect( self.make_this_subtitle_default_comboBox_text_changed) self.abort_on_errors_checkBox.stateChanged.connect(self.abort_on_errors_state_changed) self.keep_log_file_checkBox.stateChanged.connect(self.keep_log_file_state_changed) self.job_queue_layout.update_task_bar_progress_signal.connect(self.update_task_bar_progress) self.job_queue_layout.paused_done_signal.connect(self.paused_done) self.job_queue_layout.cancel_done_signal.connect(self.cancel_done) self.job_queue_layout.finished_all_jobs_signal.connect(self.finished_all_jobs) self.job_queue_layout.pause_from_error_occurred_signal.connect(self.pause_multiplexing_button_clicked) def setup_widgets(self): self.setup_mux_setting_groupBox() self.setup_job_queue_groupBox() self.setup_destination_path_label() self.setup_destination_path_lineEdit() self.setup_destination_path_button() self.setup_abort_on_errors_checkBox() self.setup_discard_old_attachments_checkBox() self.setup_keep_log_file_checkBox() self.setup_clear_job_queue_button() self.setup_tool_tip_hint() self.setup_layouts() def setup_layouts(self): self.setup_MainLayout() self.mux_setting_groupBox.setLayout(self.mux_setting_layout) self.job_queue_groupBox.setLayout(self.job_queue_layout) self.setup_mux_tools_layout_first_row() self.setup_mux_tools_layout_second_row() self.setup_mux_setting_layout() self.setLayout(self.MainLayout) # noinspection PyAttributeOutsideInit def create_widgets(self): self.MainLayout = QVBoxLayout() self.mux_setting_groupBox = QGroupBox(self) self.job_queue_groupBox = QGroupBox(self) self.mux_setting_layout = QGridLayout() self.job_queue_layout = JobQueueLayout() self.destination_path_label = QLabel() self.destination_path_lineEdit = QLineEdit() self.destination_path_button = QPushButton() self.only_keep_those_audios_checkBox = OnlyKeepThoseAudiosCheckBox() self.only_keep_those_subtitles_checkBox = OnlyKeepThoseSubtitlesCheckBox() self.only_keep_those_audios_multi_choose_comboBox = AudioTracksCheckableComboBox() self.only_keep_those_subtitles_multi_choose_comboBox = SubtitleTracksCheckableComboBox() self.make_this_audio_default_checkBox = MakeThisAudioDefaultCheckBox() self.make_this_subtitle_default_checkBox = MakeThisSubtitleDefaultCheckBox() self.make_this_audio_default_comboBox = MakeThisTrackDefaultComboBox() self.make_this_subtitle_default_comboBox = MakeThisTrackDefaultComboBox() self.abort_on_errors_checkBox = QCheckBox() self.discard_old_attachments_checkBox = QCheckBox() self.keep_log_file_checkBox = QCheckBox() self.control_queue_button = ControlQueueButton() self.clear_job_queue_button = QPushButton() self.mux_tools_layout_first_row = QHBoxLayout() self.mux_tools_layout_second_row = QHBoxLayout() self.job_queue_tools_layout = QHBoxLayout() def setup_mux_setting_layout(self): self.mux_setting_layout.addWidget(self.destination_path_label, 0, 0) self.mux_setting_layout.addWidget(self.destination_path_lineEdit, 0, 1) self.mux_setting_layout.addWidget(self.destination_path_button, 0, 2) self.mux_setting_layout.addWidget(self.only_keep_those_audios_checkBox, 1, 0) self.mux_setting_layout.addWidget(self.only_keep_those_subtitles_checkBox, 2, 0) self.mux_setting_layout.addLayout(self.mux_tools_layout_first_row, 1, 1) self.mux_setting_layout.addLayout(self.mux_tools_layout_second_row, 2, 1) def setup_mux_tools_layout_first_row(self): self.mux_tools_layout_first_row.addWidget(self.only_keep_those_audios_multi_choose_comboBox, 2) self.mux_tools_layout_first_row.addWidget(self.make_this_audio_default_checkBox, 1) self.mux_tools_layout_first_row.addWidget(self.make_this_audio_default_comboBox, 2) self.mux_tools_layout_first_row.addWidget(self.abort_on_errors_checkBox, 1) self.mux_tools_layout_first_row.addWidget(self.keep_log_file_checkBox) def setup_mux_tools_layout_second_row(self): self.mux_tools_layout_second_row.addWidget(self.only_keep_those_subtitles_multi_choose_comboBox, 2) self.mux_tools_layout_second_row.addWidget(self.make_this_subtitle_default_checkBox, 1) self.mux_tools_layout_second_row.addWidget(self.make_this_subtitle_default_comboBox, 2) self.mux_tools_layout_second_row.addWidget(self.control_queue_button, 1) self.mux_tools_layout_second_row.addWidget(self.clear_job_queue_button, 1) def setup_clear_job_queue_button(self): self.clear_job_queue_button.setText("Clear All") self.clear_job_queue_button.setIcon(GlobalFiles.CleanIcon) self.clear_job_queue_button.setDisabled(True) def setup_keep_log_file_checkBox(self): self.keep_log_file_checkBox.setText("Keep Log File") self.keep_log_file_checkBox.setToolTip("log file will located in the source folder after finished muxing") def setup_discard_old_attachments_checkBox(self): self.discard_old_attachments_checkBox.setText("Discard Old Attachments ") def setup_abort_on_errors_checkBox(self): self.abort_on_errors_checkBox.setText("Abort On Errors") self.abort_on_errors_checkBox.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) def setup_destination_path_button(self): self.destination_path_button.setIcon(GlobalFiles.SelectFolderIcon) def setup_destination_path_lineEdit(self): self.destination_path_lineEdit.setPlaceholderText("Enter Destination Folder Path") self.destination_path_lineEdit.setClearButtonEnabled(True) def setup_destination_path_label(self): self.destination_path_label.setText("Videos Destination Folder :") def setup_MainLayout(self): self.MainLayout.addWidget(self.mux_setting_groupBox) self.MainLayout.addWidget(self.job_queue_groupBox) def setup_job_queue_groupBox(self): self.job_queue_groupBox.setTitle("Job Queue") def setup_mux_setting_groupBox(self): self.mux_setting_groupBox.setTitle("Mux Setting") def paintEvent(self, event: QPaintEvent): self.update_widgets_size() super().paintEvent(event) def resizeEvent(self, event: QResizeEvent): self.job_queue_layout.update_layout() super().resizeEvent(event) def update_widgets_size(self): self.only_keep_those_subtitles_multi_choose_comboBox.resize( self.only_keep_those_audios_multi_choose_comboBox.width(), self.only_keep_those_audios_multi_choose_comboBox.height(), ) self.make_this_subtitle_default_checkBox.resize( self.make_this_audio_default_checkBox.width(), self.make_this_audio_default_checkBox.height(), ) self.make_this_subtitle_default_checkBox.move( self.make_this_audio_default_checkBox.x(), self.make_this_subtitle_default_checkBox.y(), ) self.make_this_subtitle_default_comboBox.resize( self.make_this_audio_default_comboBox.width(), self.make_this_audio_default_comboBox.height(), ) self.make_this_subtitle_default_comboBox.move( self.make_this_audio_default_comboBox.x(), self.make_this_subtitle_default_comboBox.y(), ) self.control_queue_button.move( self.abort_on_errors_checkBox.x(), self.control_queue_button.y(), ) self.clear_job_queue_button.move( self.control_queue_button.x() + self.control_queue_button.width() + 5, self.clear_job_queue_button.y(), ) def open_select_destination_folder_dialog(self): temp_folder_path = QFileDialog.getExistingDirectory(self, caption="Choose Destination Folder", dir=GlobalSetting.LAST_DIRECTORY_PATH, ) if temp_folder_path == "" or temp_folder_path.isspace(): return elif Path(temp_folder_path) == Path(GlobalSetting.VIDEO_SOURCE_PATH): invalid_dialog = InvalidPathDialog( error_message="Source and destination videos can't be in the same folder") invalid_dialog.execute() return else: self.destination_path_lineEdit.setText(str(Path(temp_folder_path))) GlobalSetting.LAST_DIRECTORY_PATH = self.destination_path_lineEdit.text() GlobalSetting.DESTINATION_FOLDER_PATH = self.destination_path_lineEdit.text() def check_destination_path(self): temp_destination_path = self.destination_path_lineEdit.text() try: if temp_destination_path == "" or temp_destination_path.isspace(): temp_destination_path = "[Empty Path]" raise Exception( "[WinError 998] Empty path is Not a valid path : " + temp_destination_path) # check if system is windows so path must have # SOME_LETTER:\ if os.name == 'nt': if temp_destination_path[1:3] != ":\\" and self.destination_path_lineEdit.text()[ 1:3] != ":/": raise Exception("[WinError 999] Not a valid path : " + temp_destination_path) makedirs(temp_destination_path, exist_ok=True) ## test if i can write into this path: test_file_name = str(time.time()) + ".txt" test_file_name_absolute = os.path.join(Path(temp_destination_path), Path(test_file_name)) try: with open(test_file_name_absolute, 'w+') as test_file: test_file.write("Test") os.remove(test_file_name_absolute) except Exception as e: write_to_log_file(e) invaild_dialog = InvalidPathDialog(window_title="Permission Denied", error_message="MKV Muxing Batch GUI lacks write " "permissions on Destination folder") invaild_dialog.execute() self.destination_path_lineEdit.setText(GlobalSetting.DESTINATION_FOLDER_PATH) return False except Exception as e: write_to_log_file(e) error_message = "" if temp_destination_path == "[Empty Path]": error_message = "Enter a valid destination path" else: error_message = temp_destination_path + "\nisn't a valid path!" invalid_dialog = InvalidPathDialog(error_message=error_message) invalid_dialog.execute() self.destination_path_lineEdit.setText(GlobalSetting.DESTINATION_FOLDER_PATH) return False if Path(temp_destination_path) == Path(GlobalSetting.VIDEO_SOURCE_PATH): invalid_dialog = InvalidPathDialog( error_message="Source and destination videos can't be in the same folder") invalid_dialog.execute() self.destination_path_lineEdit.setText(GlobalSetting.DESTINATION_FOLDER_PATH) return False GlobalSetting.DESTINATION_FOLDER_PATH = temp_destination_path return True def setup_tool_tip_hint(self): self.only_keep_those_subtitles_multi_choose_comboBox.set_tool_tip_hint() self.only_keep_those_audios_multi_choose_comboBox.set_tool_tip_hint() self.make_this_subtitle_default_checkBox.set_tool_tip_hint_no_check() self.make_this_audio_default_checkBox.set_tool_tip_hint_no_check() def add_to_queue_button_clicked(self): self.job_queue_layout.setup_queue() self.enable_muxing_setting() if not GlobalSetting.JOB_QUEUE_EMPTY: self.disable_editable_widgets() self.control_queue_button.set_state_start_multiplexing() self.clear_job_queue_button.setDisabled(False) change_global_LogFilePath() else: self.enable_editable_widgets() self.setup_enable_options_for_mkv_only_options() def tab_clicked(self): self.job_queue_layout.show_necessary_table_columns() self.setup_enable_options_for_mkv_only_options() def setup_enable_options_for_mkv_only_options(self): if GlobalSetting.JOB_QUEUE_EMPTY: if GlobalSetting.VIDEO_SOURCE_MKV_ONLY: self.only_keep_those_audios_checkBox.setEnabled(True) self.only_keep_those_subtitles_checkBox.setEnabled(True) self.make_this_audio_default_checkBox.setEnabled(True) self.make_this_subtitle_default_checkBox.setEnabled(True) self.only_keep_those_audios_checkBox.setToolTip("") self.only_keep_those_subtitles_checkBox.setToolTip("") self.make_this_audio_default_comboBox.setToolTip("") self.make_this_subtitle_default_comboBox.setToolTip("") self.setup_tool_tip_hint() else: self.only_keep_those_subtitles_checkBox.setCheckState(Qt.Unchecked) self.only_keep_those_audios_checkBox.setCheckState(Qt.Unchecked) self.make_this_audio_default_checkBox.setCheckState(Qt.Unchecked) self.make_this_subtitle_default_checkBox.setCheckState(Qt.Unchecked) self.only_keep_those_audios_checkBox.setEnabled(False) self.only_keep_those_subtitles_checkBox.setEnabled(False) self.make_this_audio_default_checkBox.setEnabled(False) self.make_this_subtitle_default_checkBox.setEnabled(False) self.only_keep_those_audios_checkBox.setToolTip("<b>[Disabled]</b> Only works when video files " "are Mkv only") self.only_keep_those_subtitles_checkBox.setToolTip("<b>[Disabled]</b> Only works when video files " "are Mkv only") self.make_this_audio_default_checkBox.setToolTip("<b>[Disabled]</b> Only works when video files " "are Mkv only") self.make_this_subtitle_default_checkBox.setToolTip("<b>[Disabled]</b> Only works when video files " "are Mkv only") self.make_this_audio_default_comboBox.setToolTip("<b>[Disabled]</b> Only works when video files " "are Mkv only") self.make_this_subtitle_default_comboBox.setToolTip("<b>[Disabled]</b> Only works when video files " "are Mkv only") self.only_keep_those_audios_multi_choose_comboBox.setToolTip( "<b>[Disabled]</b> Only works when video files " "are Mkv only") self.only_keep_those_subtitles_multi_choose_comboBox.setToolTip( "<b>[Disabled]</b> Only works when video files " "are Mkv only") def clear_job_queue_button_clicked(self): self.job_queue_layout.clear_queue() self.control_queue_button.set_state_add_to_queue() self.clear_job_queue_button.setDisabled(True) self.control_queue_button.setDisabled(False) self.enable_editable_widgets() self.enable_muxing_setting() self.setup_enable_options_for_mkv_only_options() self.update_task_bar_clear_signal.emit() def disable_editable_widgets(self): self.only_keep_those_subtitles_checkBox.setEnabled(False) self.only_keep_those_subtitles_multi_choose_comboBox.setEnabled(False) self.only_keep_those_audios_checkBox.setEnabled(False) self.only_keep_those_audios_multi_choose_comboBox.setEnabled(False) self.make_this_subtitle_default_checkBox.setEnabled(False) self.make_this_subtitle_default_comboBox.setEnabled(False) self.make_this_audio_default_checkBox.setEnabled(False) self.make_this_audio_default_comboBox.setEnabled(False) def enable_editable_widgets(self): self.only_keep_those_subtitles_checkBox.setEnabled(True) self.only_keep_those_subtitles_multi_choose_comboBox.setEnabled( self.only_keep_those_subtitles_checkBox.isChecked()) self.only_keep_those_audios_checkBox.setEnabled(True) self.only_keep_those_audios_multi_choose_comboBox.setEnabled(self.only_keep_those_audios_checkBox.isChecked()) self.make_this_subtitle_default_checkBox.setEnabled(True) self.make_this_subtitle_default_comboBox.setEnabled(self.make_this_subtitle_default_checkBox.isChecked()) self.make_this_audio_default_checkBox.setEnabled(True) self.make_this_audio_default_comboBox.setEnabled(self.make_this_audio_default_checkBox.isChecked()) def only_keep_those_audios_close_list(self): GlobalSetting.MUX_SETTING_ONLY_KEEP_THOSE_AUDIOS_LANGUAGES = self.only_keep_those_audios_multi_choose_comboBox.languages GlobalSetting.MUX_SETTING_ONLY_KEEP_THOSE_AUDIOS_TRACKS = self.only_keep_those_audios_multi_choose_comboBox.tracks def only_keep_those_subtitles_close_list(self): GlobalSetting.MUX_SETTING_ONLY_KEEP_THOSE_SUBTITLES_LANGUAGES = self.only_keep_those_subtitles_multi_choose_comboBox.languages GlobalSetting.MUX_SETTING_ONLY_KEEP_THOSE_SUBTITLES_TRACKS = self.only_keep_those_subtitles_multi_choose_comboBox.tracks def disable_make_this_subtitle_default_comboBox(self, state): self.make_this_subtitle_default_comboBox.setDisabled(state) if state: self.make_this_subtitle_default_comboBox.setCurrentIndex(-1) def disable_make_this_audio_default_comboBox(self, state): self.make_this_audio_default_comboBox.setDisabled(state) if state: self.make_this_audio_default_comboBox.setCurrentIndex(-1) def make_this_audio_default_comboBox_text_changed(self): GlobalSetting.MUX_SETTING_MAKE_THIS_AUDIO_DEFAULT_TRACK = str( self.make_this_audio_default_comboBox.currentText()) def make_this_subtitle_default_comboBox_text_changed(self): GlobalSetting.MUX_SETTING_MAKE_THIS_SUBTITLE_DEFAULT_TRACK = str( self.make_this_subtitle_default_comboBox.currentText()) def update_task_bar_progress(self, new_progress): self.update_task_bar_progress_signal.emit(new_progress) def enable_muxing_setting(self): self.destination_path_lineEdit.setEnabled(True) self.destination_path_button.setEnabled(True) self.abort_on_errors_checkBox.setEnabled(True) self.keep_log_file_checkBox.setEnabled(True) def disable_muxing_setting(self): self.destination_path_lineEdit.setEnabled(False) self.destination_path_button.setEnabled(False) self.abort_on_errors_checkBox.setEnabled(False) self.keep_log_file_checkBox.setEnabled(False) @staticmethod def abort_on_errors_state_changed(state): GlobalSetting.MUX_SETTING_ABORT_ON_ERRORS = bool(state) @staticmethod def keep_log_file_state_changed(state): GlobalSetting.MUX_SETTING_KEEP_LOG_FILE = bool(state) def start_multiplexing_button_clicked(self): at_least_one_muxing_setting_has_been_selected = check_if_at_least_one_muxing_setting_has_been_selected() if at_least_one_muxing_setting_has_been_selected: destination_path_valid = self.check_destination_path() if destination_path_valid: self.setup_log_file() self.control_queue_button.set_state_pause_multiplexing() self.disable_muxing_setting() self.job_queue_layout.start_muxing() self.start_muxing_signal.emit() self.clear_job_queue_button.setDisabled(True) def pause_multiplexing_button_clicked(self): self.job_queue_layout.pause_muxing() self.control_queue_button.setDisabled(True) self.control_queue_button.set_state_pausing_multiplexing() def paused_done(self): self.control_queue_button.set_state_resume_multiplexing() self.clear_job_queue_button.setDisabled(False) self.control_queue_button.setDisabled(False) self.update_task_bar_paused_signal.emit() def cancel_done(self): self.disable_editable_widgets() self.enable_muxing_setting() self.control_queue_button.set_state_start_multiplexing() self.clear_job_queue_button.setDisabled(False) change_global_LogFilePath() def finished_all_jobs(self): self.enable_editable_widgets() self.enable_muxing_setting() self.setup_enable_options_for_mkv_only_options() self.control_queue_button.set_state_start_multiplexing() self.control_queue_button.setDisabled(True) self.clear_job_queue_button.setDisabled(False) self.update_task_bar_clear_signal.emit() GlobalSetting.JOB_QUEUE_EMPTY = True check_if_want_to_keep_log_file() def setup_log_file(self): if self.control_queue_button.state == "START": open(GlobalFiles.LogFilePath, 'w+').close()
class ComparisonWidget(ToolWidget): def __init__(self, filename, image, parent=None): super(ComparisonWidget, self).__init__(parent) load_button = QPushButton(self.tr('Load reference image...')) self.comp_label = QLabel(self.tr('Comparison:')) self.normal_radio = QRadioButton(self.tr('Normal')) self.normal_radio.setToolTip(self.tr('Show reference (raw pixels)')) self.normal_radio.setChecked(True) self.difference_radio = QRadioButton(self.tr('Difference')) self.difference_radio.setToolTip( self.tr('Show evidence/reference difference')) self.ssim_radio = QRadioButton(self.tr('SSIM Map')) self.ssim_radio.setToolTip(self.tr('Structure similarity quality map')) self.butter_radio = QRadioButton(self.tr('Butteraugli')) self.butter_radio.setToolTip( self.tr('Butteraugli spatial changes heatmap')) self.gray_check = QCheckBox(self.tr('Grayscale')) self.gray_check.setToolTip(self.tr('Show desaturated output')) self.equalize_check = QCheckBox(self.tr('Equalized')) self.equalize_check.setToolTip(self.tr('Apply histogram equalization')) self.last_radio = self.normal_radio self.metric_button = QPushButton(self.tr('Compute metrics')) self.metric_button.setToolTip( self.tr('Image quality assessment metrics')) self.evidence = image self.reference = self.difference = self.ssim_map = self.butter_map = None basename = os.path.basename(filename) self.evidence_viewer = ImageViewer( self.evidence, None, self.tr('Evidence: {}'.format(basename))) self.reference_viewer = ImageViewer(np.full_like(self.evidence, 127), None, self.tr('Reference')) self.table_widget = QTableWidget(21, 3) self.table_widget.setHorizontalHeaderLabels( [self.tr('Metric'), self.tr('Value'), self.tr('Better')]) self.table_widget.setItem(0, 0, QTableWidgetItem(self.tr('RMSE'))) self.table_widget.setItem( 0, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)')) self.table_widget.item(0, 0).setToolTip( self. tr('Root Mean Square Error (RMSE) is commonly used to compare \n' 'the difference between the reference and evidence images \n' 'by directly computing the variation in pixel values. \n' 'The combined image is close to the reference image when \n' 'RMSE value is zero. RMSE is a good indicator of the spectral \n' 'quality of the reference image.')) self.table_widget.setItem(1, 0, QTableWidgetItem(self.tr('SAM'))) self.table_widget.setItem( 1, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)')) self.table_widget.item(1, 0).setToolTip( self. tr('It computes the spectral angle between the pixel, vector of the \n' 'evidence image and reference image. It is worked out in either \n' 'degrees or radians. It is performed on a pixel-by-pixel base. \n' 'A SAM equal to zero denotes the absence of spectral distortion.' )) self.table_widget.setItem(2, 0, QTableWidgetItem(self.tr('ERGAS'))) self.table_widget.setItem( 2, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)')) self.table_widget.item(2, 0).setToolTip( self. tr('It is used to compute the quality of reference image in terms \n' 'of normalized average error of each band of the reference image. \n' 'Increase in the value of ERGAS indicates distortion in the \n' 'reference image, lower value of ERGAS indicates that it is \n' 'similar to the reference image.')) self.table_widget.setItem(3, 0, QTableWidgetItem(self.tr('MB'))) self.table_widget.setItem( 3, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)')) self.table_widget.item(3, 0).setToolTip( self. tr('Mean Bias is the difference between the mean of the evidence \n' 'image and reference image. The ideal value is zero and indicates \n' 'that the evidence and reference images are similar. Mean value \n' 'refers to the grey level of pixels in an image.')) self.table_widget.setItem(4, 0, QTableWidgetItem(self.tr('PFE'))) self.table_widget.setItem( 4, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)')) self.table_widget.item(4, 0).setToolTip( self. tr('It computes the norm of the difference between the corresponding \n' 'pixels of the reference and fused image to the norm of the reference \n' 'image. When the calculated value is zero, it indicates that both the \n' 'reference and fused images are similar and value will be increased \n' 'when the merged image is not similar to the reference image.')) self.table_widget.setItem(5, 0, QTableWidgetItem(self.tr('PSNR'))) self.table_widget.setItem( 5, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(+' + u'\u221e' + ')')) self.table_widget.item(5, 0).setToolTip( self. tr('It is widely used metric it is computed by the number of gray levels \n' 'in the image divided by the corresponding pixels in the evidence and \n' 'the reference images. When the value is high, both images are similar.' )) self.table_widget.setItem(6, 0, QTableWidgetItem(self.tr('PSNR-B'))) self.table_widget.setItem( 6, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(+' + u'\u221e' + ')')) self.table_widget.item(6, 0).setToolTip( self.tr('PSNR with Blocking Effect Factor.')) self.table_widget.setItem(7, 0, QTableWidgetItem(self.tr('SSIM'))) self.table_widget.setItem( 7, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(1)')) self.table_widget.item(7, 0).setToolTip( self. tr('SSIM is used to compare the local patterns of pixel intensities between \n' ' the reference and fused images. The range varies between -1 to 1. \n' 'The value 1 indicates the reference and fused images are similar.' )) self.table_widget.setItem(8, 0, QTableWidgetItem(self.tr('MS-SSIM'))) self.table_widget.setItem( 8, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(1)')) self.table_widget.item(8, 0).setToolTip( self.tr('Multiscale version of SSIM.')) self.table_widget.setItem(9, 0, QTableWidgetItem(self.tr('RASE'))) self.table_widget.setItem( 9, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)')) self.table_widget.item(9, 0).setToolTip( self.tr('Relative average spectral error')) self.table_widget.setItem(10, 0, QTableWidgetItem(self.tr('SCC'))) self.table_widget.setItem( 10, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(1)')) self.table_widget.item(10, 0).setToolTip( self.tr('Spatial Correlation Coefficient')) self.table_widget.setItem(11, 0, QTableWidgetItem(self.tr('UQI'))) self.table_widget.setItem( 11, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(1)')) self.table_widget.item(11, 0).setToolTip( self.tr('Universal Image Quality Index')) self.table_widget.setItem(12, 0, QTableWidgetItem(self.tr('VIF-P'))) self.table_widget.setItem( 12, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(1)')) self.table_widget.item(12, 0).setToolTip( self.tr('Pixel-based Visual Information Fidelity')) self.table_widget.setItem(13, 0, QTableWidgetItem(self.tr('SSIMulacra'))) self.table_widget.setItem( 13, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)')) self.table_widget.item(13, 0).setToolTip( self.tr('Structural SIMilarity Unveiling Local ' 'And Compression Related Artifacts')) self.table_widget.setItem(14, 0, QTableWidgetItem(self.tr('Butteraugli'))) self.table_widget.setItem( 14, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)')) self.table_widget.item(14, 0).setToolTip( self.tr('Estimate psychovisual error')) self.table_widget.setItem(15, 0, QTableWidgetItem(self.tr('Correlation'))) self.table_widget.setItem( 15, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(1)')) self.table_widget.item(15, 0).setToolTip(self.tr('Histogram correlation')) self.table_widget.setItem(16, 0, QTableWidgetItem(self.tr('Chi-Square'))) self.table_widget.setItem( 16, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)')) self.table_widget.item(16, 0).setToolTip(self.tr('Histogram Chi-Square')) self.table_widget.setItem(17, 0, QTableWidgetItem(self.tr('Chi-Square 2'))) self.table_widget.setItem( 17, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)')) self.table_widget.item(17, 0).setToolTip(self.tr('Alternative Chi-Square')) self.table_widget.setItem(18, 0, QTableWidgetItem(self.tr('Intersection'))) self.table_widget.setItem( 18, 2, QTableWidgetItem(QIcon('gui/icons/high.svg'), '(+' + u'\u221e' + ')')) self.table_widget.item(18, 0).setToolTip(self.tr('Histogram intersection')) self.table_widget.setItem(19, 0, QTableWidgetItem(self.tr('Hellinger'))) self.table_widget.setItem( 19, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)')) self.table_widget.item(19, 0).setToolTip( self.tr('Histogram Hellinger distance')) self.table_widget.setItem(20, 0, QTableWidgetItem(self.tr('Divergence'))) self.table_widget.setItem( 20, 2, QTableWidgetItem(QIcon('gui/icons/low.svg'), '(0)')) self.table_widget.item(20, 0).setToolTip( self.tr('Kullback-Leibler divergence')) for i in range(self.table_widget.rowCount()): modify_font(self.table_widget.item(i, 0), bold=True) self.table_widget.setSelectionMode(QAbstractItemView.SingleSelection) self.table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table_widget.resizeColumnsToContents() self.table_widget.setMaximumWidth(250) self.table_widget.setAlternatingRowColors(True) self.stopped = False self.comp_label.setEnabled(False) self.normal_radio.setEnabled(False) self.difference_radio.setEnabled(False) self.ssim_radio.setEnabled(False) self.butter_radio.setEnabled(False) self.gray_check.setEnabled(False) self.equalize_check.setEnabled(False) self.metric_button.setEnabled(False) self.table_widget.setEnabled(False) load_button.clicked.connect(self.load) self.normal_radio.clicked.connect(self.change) self.difference_radio.clicked.connect(self.change) self.butter_radio.clicked.connect(self.change) self.gray_check.stateChanged.connect(self.change) self.equalize_check.stateChanged.connect(self.change) self.ssim_radio.clicked.connect(self.change) self.evidence_viewer.viewChanged.connect( self.reference_viewer.changeView) self.reference_viewer.viewChanged.connect( self.evidence_viewer.changeView) self.metric_button.clicked.connect(self.metrics) top_layout = QHBoxLayout() top_layout.addWidget(load_button) top_layout.addStretch() top_layout.addWidget(self.comp_label) top_layout.addWidget(self.normal_radio) top_layout.addWidget(self.difference_radio) top_layout.addWidget(self.ssim_radio) top_layout.addWidget(self.butter_radio) top_layout.addWidget(self.gray_check) top_layout.addWidget(self.equalize_check) metric_layout = QVBoxLayout() index_label = QLabel(self.tr('Image Quality Assessment')) index_label.setAlignment(Qt.AlignCenter) modify_font(index_label, bold=True) metric_layout.addWidget(index_label) metric_layout.addWidget(self.table_widget) metric_layout.addWidget(self.metric_button) center_layout = QHBoxLayout() center_layout.addWidget(self.evidence_viewer) center_layout.addWidget(self.reference_viewer) center_layout.addLayout(metric_layout) main_layout = QVBoxLayout() main_layout.addLayout(top_layout) main_layout.addLayout(center_layout) self.setLayout(main_layout) def load(self): filename, basename, reference = load_image(self) if filename is None: return if reference.shape != self.evidence.shape: QMessageBox.critical( self, self.tr('Error'), self.tr('Evidence and reference must have the same size!')) return self.reference = reference self.reference_viewer.set_title( self.tr('Reference: {}'.format(basename))) self.difference = norm_mat(cv.absdiff(self.evidence, self.reference)) self.comp_label.setEnabled(True) self.normal_radio.setEnabled(True) self.difference_radio.setEnabled(True) self.ssim_radio.setEnabled(False) self.butter_radio.setEnabled(False) self.gray_check.setEnabled(True) self.equalize_check.setEnabled(True) self.metric_button.setEnabled(True) for i in range(self.table_widget.rowCount()): self.table_widget.setItem(i, 1, QTableWidgetItem()) self.normal_radio.setChecked(True) self.table_widget.setEnabled(False) self.change() def change(self): if self.normal_radio.isChecked(): result = self.reference self.gray_check.setEnabled(False) self.equalize_check.setEnabled(False) self.last_radio = self.normal_radio elif self.difference_radio.isChecked(): result = self.difference self.gray_check.setEnabled(True) self.equalize_check.setEnabled(True) self.last_radio = self.difference_radio elif self.ssim_radio.isChecked(): result = self.ssim_map self.gray_check.setEnabled(False) self.equalize_check.setEnabled(True) self.last_radio = self.ssim_radio elif self.butter_radio.isChecked(): result = self.butter_map self.gray_check.setEnabled(True) self.equalize_check.setEnabled(False) self.last_radio = self.butter_radio else: self.last_radio.setChecked(True) return if self.equalize_check.isChecked(): result = equalize_img(result) if self.gray_check.isChecked(): result = desaturate(result) self.reference_viewer.update_original(result) def metrics(self): progress = QProgressDialog(self.tr('Computing metrics...'), self.tr('Cancel'), 1, self.table_widget.rowCount(), self) progress.canceled.connect(self.cancel) progress.setWindowModality(Qt.WindowModal) img1 = cv.cvtColor(self.evidence, cv.COLOR_BGR2GRAY) img2 = cv.cvtColor(self.reference, cv.COLOR_BGR2GRAY) x = img1.astype(np.float64) y = img2.astype(np.float64) rmse = self.rmse(x, y) progress.setValue(1) if self.stopped: return sam = sewar.sam(img1, img2) progress.setValue(2) if self.stopped: return ergas = sewar.ergas(img1, img2) progress.setValue(3) if self.stopped: return mb = self.mb(x, y) progress.setValue(4) if self.stopped: return pfe = self.pfe(x, y) progress.setValue(5) if self.stopped: return psnr = self.psnr(x, y) progress.setValue(6) if self.stopped: return try: psnrb = sewar.psnrb(img1, img2) except NameError: # FIXME: C'\`e un bug in psnrb (https://github.com/andrewekhalel/sewar/issues/17) psnrb = 0 progress.setValue(7) if self.stopped: return ssim, self.ssim_map = self.ssim(x, y) progress.setValue(8) if self.stopped: return mssim = sewar.msssim(img1, img2).real progress.setValue(9) if self.stopped: return rase = sewar.rase(img1, img2) progress.setValue(10) if self.stopped: return scc = sewar.scc(img1, img2) progress.setValue(11) if self.stopped: return uqi = sewar.uqi(img1, img2) progress.setValue(12) if self.stopped: return vifp = sewar.vifp(img1, img2) progress.setValue(13) if self.stopped: return ssimul = self.ssimul(img1, img2) progress.setValue(14) if self.stopped: return butter, self.butter_map = self.butter(img1, img2) progress.setValue(15) if self.stopped: return sizes = [256, 256, 256] ranges = [0, 256] * 3 channels = [0, 1, 2] hist1 = cv.calcHist([self.evidence], channels, None, sizes, ranges) hist2 = cv.calcHist([self.reference], channels, None, sizes, ranges) correlation = cv.compareHist(hist1, hist2, cv.HISTCMP_CORREL) progress.setValue(16) if self.stopped: return chi_square = cv.compareHist(hist1, hist2, cv.HISTCMP_CHISQR) progress.setValue(17) if self.stopped: return chi_square2 = cv.compareHist(hist1, hist2, cv.HISTCMP_CHISQR_ALT) progress.setValue(18) if self.stopped: return intersection = cv.compareHist(hist1, hist2, cv.HISTCMP_INTERSECT) progress.setValue(19) if self.stopped: return hellinger = cv.compareHist(hist1, hist2, cv.HISTCMP_HELLINGER) progress.setValue(20) if self.stopped: return divergence = cv.compareHist(hist1, hist2, cv.HISTCMP_KL_DIV) progress.setValue(21) self.table_widget.setItem(0, 1, QTableWidgetItem('{:.2f}'.format(rmse))) self.table_widget.setItem(1, 1, QTableWidgetItem('{:.4f}'.format(sam))) self.table_widget.setItem(2, 1, QTableWidgetItem('{:.2f}'.format(ergas))) self.table_widget.setItem(3, 1, QTableWidgetItem('{:.4f}'.format(mb))) self.table_widget.setItem(4, 1, QTableWidgetItem('{:.2f}'.format(pfe))) if psnr > 0: self.table_widget.setItem( 5, 1, QTableWidgetItem('{:.2f} dB'.format(psnr))) else: self.table_widget.setItem( 5, 1, QTableWidgetItem('+' + u'\u221e' + ' dB')) self.table_widget.setItem(6, 1, QTableWidgetItem('{:.2f}'.format(psnrb))) self.table_widget.setItem(7, 1, QTableWidgetItem('{:.4f}'.format(ssim))) self.table_widget.setItem(8, 1, QTableWidgetItem('{:.4f}'.format(mssim))) self.table_widget.setItem(9, 1, QTableWidgetItem('{:.2f}'.format(rase))) self.table_widget.setItem(10, 1, QTableWidgetItem('{:.4f}'.format(scc))) self.table_widget.setItem(11, 1, QTableWidgetItem('{:.4f}'.format(uqi))) self.table_widget.setItem(12, 1, QTableWidgetItem('{:.4f}'.format(vifp))) self.table_widget.setItem(13, 1, QTableWidgetItem('{:.4f}'.format(ssimul))) self.table_widget.setItem(14, 1, QTableWidgetItem('{:.2f}'.format(butter))) self.table_widget.setItem( 15, 1, QTableWidgetItem('{:.2f}'.format(correlation))) self.table_widget.setItem( 16, 1, QTableWidgetItem('{:.2f}'.format(chi_square))) self.table_widget.setItem( 17, 1, QTableWidgetItem('{:.2f}'.format(chi_square2))) self.table_widget.setItem( 18, 1, QTableWidgetItem('{:.2f}'.format(intersection))) self.table_widget.setItem(19, 1, QTableWidgetItem('{:.2f}'.format(hellinger))) self.table_widget.setItem( 20, 1, QTableWidgetItem('{:.2f}'.format(divergence))) self.table_widget.resizeColumnsToContents() self.table_widget.setEnabled(True) self.metric_button.setEnabled(False) self.ssim_radio.setEnabled(True) self.butter_radio.setEnabled(True) def cancel(self): self.stopped = True @staticmethod def rmse(x, y): return np.sqrt(np.mean(np.square(x - y))) @staticmethod def mb(x, y): mx = np.mean(x) my = np.mean(y) return (mx - my) / mx @staticmethod def pfe(x, y): return np.linalg.norm(x - y) / np.linalg.norm(x) * 100 @staticmethod def ssim(x, y): c1 = 6.5025 c2 = 58.5225 k = (11, 11) s = 1.5 x2 = x**2 y2 = y**2 xy = x * y mu_x = cv.GaussianBlur(x, k, s) mu_y = cv.GaussianBlur(y, k, s) mu_x2 = mu_x**2 mu_y2 = mu_y**2 mu_xy = mu_x * mu_y s_x2 = cv.GaussianBlur(x2, k, s) - mu_x2 s_y2 = cv.GaussianBlur(y2, k, s) - mu_y2 s_xy = cv.GaussianBlur(xy, k, s) - mu_xy t1 = 2 * mu_xy + c1 t2 = 2 * s_xy + c2 t3 = t1 * t2 t1 = mu_x2 + mu_y2 + c1 t2 = s_x2 + s_y2 + c2 t1 *= t2 ssim_map = cv.divide(t3, t1) ssim = cv.mean(ssim_map)[0] return ssim, 255 - norm_mat(ssim_map, to_bgr=True) @staticmethod def corr(x, y): return np.corrcoef(x, y)[0, 1] @staticmethod def psnr(x, y): k = np.mean(np.square(x - y)) if k == 0: return -1 return 20 * math.log10((255**2) / k) @staticmethod def butter(x, y): try: exe = butter_exe() if exe is None: raise FileNotFoundError temp_dir = QTemporaryDir() if temp_dir.isValid(): filename1 = os.path.join(temp_dir.path(), 'img1.png') cv.imwrite(filename1, x) filename2 = os.path.join(temp_dir.path(), 'img2.png') cv.imwrite(filename2, y) filename3 = os.path.join(temp_dir.path(), 'map.ppm') p = run([exe, filename1, filename2, filename3], stdout=PIPE) value = float(p.stdout) heatmap = cv.imread(filename3, cv.IMREAD_COLOR) return value, heatmap except FileNotFoundError: return -1, cv.cvtColor(np.full_like(x, 127), cv.COLOR_GRAY2BGR) @staticmethod def ssimul(x, y): try: exe = ssimul_exe() if exe is None: raise FileNotFoundError temp_dir = QTemporaryDir() if temp_dir.isValid(): filename1 = os.path.join(temp_dir.path(), 'img1.png') cv.imwrite(filename1, x) filename2 = os.path.join(temp_dir.path(), 'img2.png') cv.imwrite(filename2, y) p = run([exe, filename1, filename2], stdout=PIPE) value = float(p.stdout) return value except FileNotFoundError: return -1
class MainView(QMainWindow): applicationClosed = Signal() def __init__(self): super().__init__() menuBar = self.menuBar() menuBar.addMenu(self.createFileMenu()) menuBar.addMenu(self.createSettingsMenu()) menuBar.addMenu(self.createHelpMenu()) self.statusBar().showMessage("Ready") connectionStatusGroupBox = self.createConnectionStatusGroup() positionIndicationGroupBox = self.createPositionIndicationGroup() turnTableControlGroupBox = self.createTurnTableControlGroup() mainWidgetLayout = QVBoxLayout() mainWidgetLayout.addWidget(connectionStatusGroupBox) mainWidgetLayout.addWidget(positionIndicationGroupBox) mainWidgetLayout.addWidget(turnTableControlGroupBox) mainWidgetGroup = QGroupBox() mainWidgetGroup.setLayout(mainWidgetLayout) self.setCentralWidget(mainWidgetGroup) #------------------------------------------------------------------------------ # Interface Creation Helper Functions #------------------------------------------------------------------------------ def createFileMenu(self): self.loadZeroPositionDataAction = QAction("Load Zero Position", self) self.saveZeroPositionDataAction = QAction("Save Zero Position", self) exitAction = QAction( "E&xit", self, shortcut="Ctrl+Q", statusTip="Exit the application", triggered=self.close ) fileMenu = QMenu("File") fileMenu.addAction(self.loadZeroPositionDataAction) fileMenu.addAction(self.saveZeroPositionDataAction) fileMenu.addSeparator() fileMenu.addAction(exitAction) return fileMenu def createSettingsMenu(self): self.applicationSettingsAction = QAction("Settings", self) settingsManagerMenu = QMenu("Settings") settingsManagerMenu.addAction(self.applicationSettingsAction) return settingsManagerMenu def createHelpMenu(self): helpViewAction = QAction("Help", self) aboutViewAction = QAction("About", self) helpMenu = QMenu("Help") helpMenu.addAction(helpViewAction) helpMenu.addAction(aboutViewAction) return helpMenu def createPositionIndicationGroup(self): self.currentPositionLineEdit = createReadOnlyLineEdit("+0.000") self.targetPositionLineEdit = createReadOnlyLineEdit("+0.000") self.positionErrorLineEdit = createReadOnlyLineEdit("+0.000") self.setCurrentPositionAsZeroButton = QPushButton("Set As Zero") self.resetZeroPositionButton = QPushButton("Reset Zero") positionInformationLayout = QGridLayout() positionInformationLayout.addWidget(createPositionLabel("Current"), 0, 0) positionInformationLayout.addWidget(createPositionLabel("Target"), 0, 1) positionInformationLayout.addWidget(createPositionLabel("Error"), 0, 2) positionInformationLayout.addWidget(self.currentPositionLineEdit, 1, 0) positionInformationLayout.addWidget(self.targetPositionLineEdit, 1, 1) positionInformationLayout.addWidget(self.positionErrorLineEdit, 1, 2) positionInformationLayout.addWidget(self.setCurrentPositionAsZeroButton, 2, 0) positionInformationLayout.addWidget(self.resetZeroPositionButton, 3, 0) positionGroupBox = QGroupBox("Azimuth") positionGroupBox.setLayout(positionInformationLayout) return positionGroupBox def createTurnTableControlGroup(self): self.gotoPositionSpinBox = createDoubleSpinBox() self.stepSizeSpinBox = createDoubleSpinBox() self.goPushButton = QPushButton("Go") self.goPushButton.setEnabled(False) self.goPushButton.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_G)) self.stepPushButton = QPushButton("Step") self.stepPushButton.setEnabled(False) self.stepPushButton.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_S)) self.enableSteppingCheckBox = QCheckBox("Enable Stepping") self.enableSteppingCheckBox.setEnabled(False) self.enableSteppingCheckBox.clicked.connect(self.stepPushButton.setEnabled) self.stopPushButton = QPushButton("STOP") self.stopPushButton.setEnabled(False) self.stopPushButton.pressed.connect(lambda: self.motorVoltageSlider.setValue(0.000)) self.stopPushButton.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_T)) turnTableVoltageControlGroup = self.createTurnTableVoltageControlGroup() turnTableControlGridLayout = QGridLayout() turnTableControlGridLayout.addWidget(QLabel("Go To Position:"), 0, 0) turnTableControlGridLayout.addWidget(self.gotoPositionSpinBox, 0, 1) turnTableControlGridLayout.addWidget(self.goPushButton, 0, 2) turnTableControlGridLayout.addWidget(QLabel("Step Size:"), 1, 0) turnTableControlGridLayout.addWidget(self.stepSizeSpinBox, 1, 1) turnTableControlGridLayout.addWidget(self.stepPushButton, 1, 2) turnTableControlGridLayout.addWidget(self.enableSteppingCheckBox, 1, 3) turnTableControlGridLayout.addWidget(self.stopPushButton, 2, 3) turnTableControlGridLayout.addWidget(turnTableVoltageControlGroup, 3, 0, 3, 4) turnTableGroupBox = QGroupBox("Turn Table Controls") turnTableGroupBox.setLayout(turnTableControlGridLayout) return turnTableGroupBox def createTurnTableVoltageControlGroup(self): self.motorVoltageSlider = DoubleSlider(orientation=Qt.Orientation.Horizontal) self.motorVoltageSlider.setTickPosition(QSlider.TickPosition.TicksBothSides) self.motorVoltageSlider.setTickInterval(200) self.motorVoltageSlider.setEnabled(False) self.motorVoltageSliderValueSpinBox = createDoubleSpinBox() self.motorVoltageSliderValueSpinBox.setEnabled(False) self.motorVoltageSliderValueSpinBox.valueChanged.connect(self.motorVoltageSlider.setValue) self.motorVoltageSlider.doubleValueChanged.connect(self.motorVoltageSliderValueSpinBox.setValue) self.resetVoltageButton = QPushButton("Reset") self.resetVoltageButton.setEnabled(False) self.resetVoltageButton.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_R)) self.resetVoltageButton.pressed.connect(lambda: self.motorVoltageSlider.setValue(0.000)) sliderGridLayout = QGridLayout() sliderGridLayout.addWidget(QLabel("Min Voltage"), 0, 0, 1, 1) sliderGridLayout.addWidget(QLabel("Max Voltage"), 0, 4, 1, 1) sliderGridLayout.addWidget(self.motorVoltageSlider, 1, 0, 1, 5) sliderGridLayout.addWidget(self.motorVoltageSliderValueSpinBox, 2, 1, 1, 1) sliderGridLayout.addWidget(self.resetVoltageButton, 2, 4, 1, 1) voltageControlGroupBox = QGroupBox() voltageControlGroupBox.setLayout(sliderGridLayout) return voltageControlGroupBox def createConnectionStatusGroup(self): connectionStatusGroup = QGroupBox("Connection Status") self.shaftEncoderConnectionStatusLineEdit = createReadOnlyLineEdit("Disconnected") self.motorControllerConnectionStatusLineEdit = createReadOnlyLineEdit("Disconnected") self.watchDogConnectionStatusLineEdit = createReadOnlyLineEdit("Disconnected") self.tcpServerConnectionStatusLineEdit = createReadOnlyLineEdit("Disconnected") self.connectButton = QPushButton("Connect") self.disconnectButton = QPushButton("Disconnect") self.disconnectButton.setEnabled(False) connectionButtonLayout = QHBoxLayout() connectionButtonLayout.addWidget(self.connectButton) connectionButtonLayout.addWidget(self.disconnectButton) connectionStatusLayout = QFormLayout() connectionStatusLayout.addRow(QLabel("Shaft Encoder: "), self.shaftEncoderConnectionStatusLineEdit) connectionStatusLayout.addRow(QLabel("Motor Controller: "), self.motorControllerConnectionStatusLineEdit) connectionStatusLayout.addRow(QLabel("Watchdog: "), self.watchDogConnectionStatusLineEdit) connectionStatusLayout.addRow(QLabel("TCP Server: "), self.tcpServerConnectionStatusLineEdit) connectionStatusLayout.addRow(connectionButtonLayout) connectionStatusGroup.setLayout(connectionStatusLayout) return connectionStatusGroup def closeEvent(self, event): messageBox = QMessageBox() messageBox.setText("The Turn Table is currently still running.") messageBox.setInformativeText("Are you sure you want to exit?") messageBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) messageBox.setDefaultButton(QMessageBox.Cancel) response = messageBox.exec_() if response == QMessageBox.Ok: self.applicationClosed.emit() event.accept() elif response == QMessageBox.Cancel: event.ignore() #------------------------------------------------------------------------------ # Callback functions for handling GUI events #------------------------------------------------------------------------------ def updatePositionLineEdits(self, newPosition): updatePositionLineEdit(self.currentPositionLineEdit, newPosition.current) updatePositionLineEdit(self.targetPositionLineEdit, newPosition.target) updatePositionLineEdit(self.positionErrorLineEdit, newPosition.error) def updateConnectionStatusLineEdits(self, shaftEncoder: bool, motorController: bool, watchdog: bool, tcpServer: bool): updateConnectionStatusLineEdit(self.shaftEncoderConnectionStatusLineEdit, shaftEncoder) updateConnectionStatusLineEdit(self.motorControllerConnectionStatusLineEdit, motorController) updateConnectionStatusLineEdit(self.watchDogConnectionStatusLineEdit, watchdog) updateConnectionStatusLineEdit(self.tcpServerConnectionStatusLineEdit, tcpServer) def toggleControls(self): toggleControlEnable(self.enableSteppingCheckBox) toggleControlEnable(self.stopPushButton) toggleControlEnable(self.goPushButton) toggleControlEnable(self.connectButton) toggleControlEnable(self.disconnectButton) toggleControlEnable(self.motorVoltageSlider) toggleControlEnable(self.motorVoltageSliderValueSpinBox) toggleControlEnable(self.resetVoltageButton)
class MixedDistributionChart(QDialog): def __init__(self, parent=None, show_mode=True, toolbar=False, use_animation=False): flags = Qt.Window | Qt.WindowTitleHint | Qt.CustomizeWindowHint | Qt.WindowCloseButtonHint super().__init__(parent=parent, f=flags) self.setWindowTitle(self.tr("Mixed Distribution Chart")) self.figure = plt.figure(figsize=(4, 3)) self.axes = self.figure.subplots() self.canvas = FigureCanvas(self.figure) self.toolbar = NavigationToolbar(self.canvas, self) self.main_layout = QGridLayout(self) self.main_layout.addWidget(self.toolbar, 0, 0, 1, 2) self.main_layout.addWidget(self.canvas, 1, 0, 1, 2) if not toolbar: self.toolbar.hide() self.supported_scales = [("log-linear", self.tr("Log-linear")), ("log", self.tr("Log")), ("phi", self.tr("φ")), ("linear", self.tr("Linear"))] self.scale_label = QLabel(self.tr("Scale")) self.scale_combo_box = QComboBox() self.scale_combo_box.addItems([name for key, name in self.supported_scales]) self.scale_combo_box.currentIndexChanged.connect(self.update_chart) self.main_layout.addWidget(self.scale_label, 2, 0) self.main_layout.addWidget(self.scale_combo_box, 2, 1) self.interval_label = QLabel(self.tr("Interval [ms]")) self.interval_input = QSpinBox() self.interval_input.setRange(0, 10000) self.interval_input.setValue(30) self.interval_input.valueChanged.connect(self.update_animation) self.main_layout.addWidget(self.interval_label, 3, 0) self.main_layout.addWidget(self.interval_input, 3, 1) self.repeat_check_box = QCheckBox(self.tr("Repeat")) self.repeat_check_box.setChecked(False) self.repeat_check_box.stateChanged.connect(self.update_animation) self.save_button = QPushButton(self.tr("Save")) self.save_button.clicked.connect(self.save_animation) self.main_layout.addWidget(self.repeat_check_box, 4, 0) self.main_layout.addWidget(self.save_button, 4, 1) self.show_mode = show_mode self.animation = None self.last_model = None self.last_result = None if not use_animation: self.interval_label.setVisible(False) self.interval_input.setVisible(False) self.repeat_check_box.setVisible(False) self.save_button.setVisible(False) self.normal_msg = QMessageBox(parent=self) self.file_dialog = QFileDialog(parent=self) @property def scale(self) -> str: index = self.scale_combo_box.currentIndex() key, name = self.supported_scales[index] return key @property def transfer(self) -> typing.Callable: if self.scale == "log-linear": return lambda classes_φ: convert_φ_to_μm(classes_φ) elif self.scale == "log": return lambda classes_φ: np.log(convert_φ_to_μm(classes_φ)) elif self.scale == "phi": return lambda classes_φ: classes_φ elif self.scale == "linear": return lambda classes_φ: convert_φ_to_μm(classes_φ) @property def xlabel(self) -> str: if self.scale == "log-linear": return self.tr("Grain-size [μm]") elif self.scale == "log": return self.tr("Ln(grain-size in μm)") elif self.scale == "phi": return self.tr("Grain-size [φ]") elif self.scale == "linear": return self.tr("Grain-size [μm]") @property def ylabel(self) -> str: return self.tr("Frequency") @property def xlog(self) -> bool: if self.scale == "log-linear": return True else: return False @property def interval(self) -> float: return self.interval_input.value() @property def repeat(self) -> bool: return self.repeat_check_box.isChecked() def show_demo(self): demo_model = get_demo_view_model() self.show_model(demo_model) def update_chart(self): if self.last_model is not None: self.show_model(self.last_model) elif self.last_result is not None: self.show_result(self.last_result) def update_animation(self): if self.last_result is not None: self.show_result(self.last_result) def show_model(self, model: SSUViewModel, quick=False): if self.animation is not None: self.animation._stop() self.animation = None if not quick: self.last_result = None self.last_model = model self.interval_label.setEnabled(False) self.interval_input.setEnabled(False) self.repeat_check_box.setEnabled(False) self.save_button.setEnabled(False) self.axes.clear() x = self.transfer(model.classes_φ) if self.xlog: self.axes.set_xscale("log") self.axes.set_title(model.title) self.axes.set_xlabel(self.xlabel) self.axes.set_ylabel(self.ylabel) self.target = self.axes.plot(x, model.target, c="#ffffff00", marker=".", ms=8, mfc="black", mew=0.0, label=self.tr("Target"))[0] # scatter can not be modified from the tool bar # self.target = self.axes.scatter(x, model.target, c="black", s=1) self.axes.set_xlim(x[0], x[-1]) self.axes.set_ylim(0.0, round(np.max(model.target)*1.2, 2)) self.mixed = self.axes.plot(x, model.mixed, c="black", label=self.tr("Mixed"))[0] self.components = [self.axes.plot(x, distribution*fraction, c=plt.get_cmap()(i), label=model.component_prefix+str(i+1))[0] for i, (distribution, fraction) in enumerate(zip(model.distributions, model.fractions))] if self.show_mode: modes = [self.transfer(model.classes_φ[np.unravel_index(np.argmax(distribution), distribution.shape)]) for distribution in model.distributions] colors = [plt.get_cmap()(i) for i in range(model.n_components)] self.vlines = self.axes.vlines(modes, 0.0, round(np.max(model.target)*1.2, 2), colors=colors) if model.n_components < 6: self.axes.legend(loc="upper left") self.figure.tight_layout() self.canvas.draw() else: self.mixed.set_ydata(model.mixed) for comp, distribution, fraction in zip(self.components, model.distributions, model.fractions): comp.set_ydata(distribution*fraction) if self.show_mode: modes = [self.transfer(model.classes_φ[np.unravel_index(np.argmax(distribution), distribution.shape)]) for distribution in model.distributions] self.vlines.set_offsets(modes) self.canvas.draw() def show_result(self, result: SSUResult): if self.animation is not None: self.animation._stop() self.animation = None self.last_model = None self.last_result = result self.interval_label.setEnabled(True) self.interval_input.setEnabled(True) self.repeat_check_box.setEnabled(True) self.save_button.setEnabled(True) models = iter(result.view_models) first = next(models) x = self.transfer(first.classes_φ) self.axes.cla() if self.xlog: self.axes.set_xscale("log") self.axes.set_title(first.title) self.axes.set_xlabel(self.xlabel) self.axes.set_ylabel(self.ylabel) self.target = self.axes.plot(x, first.target, c="#ffffff00", marker=".", ms=8, mfc="black", mew=0.0)[0] self.axes.set_xlim(x[0], x[-1]) self.axes.set_ylim(0.0, round(np.max(first.target)*1.2, 2)) self.figure.tight_layout() # self.canvas.draw() colors = [plt.get_cmap()(i) for i in range(first.n_components)] def init(): model = first self.mixed = self.axes.plot(x, model.mixed, c="black")[0] self.components = [self.axes.plot(x, distribution*fraction, c=plt.get_cmap()(i))[0] for i, (distribution, fraction) in enumerate(zip(model.distributions, model.fractions))] if self.show_mode: modes = [self.transfer(model.classes_φ[np.unravel_index(np.argmax(distribution), distribution.shape)]) for distribution in model.distributions] self.vlines = self.axes.vlines(modes, 0.0, round(np.max(model.target)*1.2, 2), colors=colors) return self.mixed, self.vlines, *self.components def animate(current): model = current self.mixed.set_ydata(current.mixed) for line, distribution, fraction in zip(self.components, model.distributions, model.fractions): line.set_ydata(distribution*fraction) if self.show_mode: self.vlines.remove() modes = [self.transfer(model.classes_φ[np.unravel_index(np.argmax(distribution), distribution.shape)]) for distribution in model.distributions] self.vlines = self.axes.vlines(modes, 0.0, round(np.max(model.target)*1.2, 2), colors=colors) return self.mixed, self.vlines, *self.components self.animation = FuncAnimation(self.figure, animate, frames=models, init_func=init, interval=self.interval, blit=True, repeat=self.repeat, repeat_delay=3.0, save_count=result.n_iterations) def save_animation(self): if self.last_result is not None: filename, format_str = self.file_dialog.getSaveFileName(self, self.tr("Save the animation of this SSU result"), None, self.tr("MPEG-4 Video File (*.mp4);;Graphics Interchange Format (*.gif)")) if filename is None or filename == "": return progress = QProgressDialog(self) progress.setRange(0, 100) progress.setLabelText(self.tr("Saving Animation [{0} Frames]").format(self.last_result.n_iterations)) canceled = False def save_callback(i, n): if progress.wasCanceled(): nonlocal canceled canceled = True raise StopIteration() progress.setValue((i+1)/n*100) QCoreApplication.processEvents() self.show_result(self.last_result) # plt.rcParams["savefig.dpi"] = 120.0 if "*.gif" in format_str: if not ImageMagickWriter.isAvailable(): self.normal_msg.setWindowTitle(self.tr("Error")) self.normal_msg.setText(self.tr("ImageMagick is not installed, please download and install it from its offical website (https://imagemagick.org/index.php).")) self.normal_msg.exec_() else: self.animation.save(filename, writer="imagemagick", fps=30, progress_callback=save_callback) elif "*.mp4" in format_str: if not FFMpegWriter.isAvailable(): self.normal_msg.setWindowTitle(self.tr("Error")) self.normal_msg.setText(self.tr("FFMpeg is not installed, please download and install it from its offical website (https://ffmpeg.org/).")) self.normal_msg.exec_() else: self.animation.save(filename, writer="ffmpeg", fps=30, progress_callback=save_callback) # plt.rcParams["savefig.dpi"] = 300.0 if not canceled: progress.setValue(100)
class Find(QDialog): def __init__(self, parent=None): QDialog.__init__(self, parent) self.parent = parent self.lastMatch = None self.initUI() def initUI(self): # Button to search the document for something findButton = QPushButton("Find", self) findButton.clicked.connect(self.find) # Button to replace the last finding replaceButton = QPushButton("Replace", self) replaceButton.clicked.connect(self.replace) # Button to remove all findings allButton = QPushButton("Replace all", self) allButton.clicked.connect(self.replaceAll) # Normal mode - radio button self.normalRadio = QRadioButton("Normal", self) self.normalRadio.toggled.connect(self.normalMode) # Regular Expression Mode - radio button self.regexRadio = QRadioButton("RegEx", self) self.regexRadio.toggled.connect(self.regexMode) # The field into which to type the query self.findField = QTextEdit(self) self.findField.resize(250, 50) # The field into which to type the text to replace the # queried text self.replaceField = QTextEdit(self) self.replaceField.resize(250, 50) optionsLabel = QLabel("Options: ", self) # Case Sensitivity option self.caseSens = QCheckBox("Case sensitive", self) # Whole Words option self.wholeWords = QCheckBox("Whole words", self) # Layout the objects on the screen layout = QGridLayout() layout.addWidget(self.findField, 1, 0, 1, 4) layout.addWidget(self.normalRadio, 2, 2) layout.addWidget(self.regexRadio, 2, 3) layout.addWidget(findButton, 2, 0, 1, 2) layout.addWidget(self.replaceField, 3, 0, 1, 4) layout.addWidget(replaceButton, 4, 0, 1, 2) layout.addWidget(allButton, 4, 2, 1, 2) # Add some spacing spacer = QWidget(self) spacer.setFixedSize(0, 10) layout.addWidget(spacer, 5, 0) layout.addWidget(optionsLabel, 6, 0) layout.addWidget(self.caseSens, 6, 1) layout.addWidget(self.wholeWords, 6, 2) self.setGeometry(300, 300, 360, 250) self.setWindowTitle("Find and Replace") self.setLayout(layout) # By default the normal mode is activated self.normalRadio.setChecked(True) def find(self): # Grab the parent's text text = self.parent.paged_text_edit.toPlainText() # And the text to find query = self.findField.toPlainText() # If the 'Whole Words' checkbox is checked, we need to append # and prepend a non-alphanumeric character if self.wholeWords.isChecked(): query = r'\W' + query + r'\W' # By default regexes are case sensitive but usually a search isn't # case sensitive by default, so we need to switch this around here flags = 0 if self.caseSens.isChecked() else re.I # Compile the pattern pattern = re.compile(query, flags) # If the last match was successful, start at position after the last # match's start, else at 0 start = self.lastMatch.start() + 1 if self.lastMatch else 0 # The actual search self.lastMatch = pattern.search(text, start) if self.lastMatch: start = self.lastMatch.start() end = self.lastMatch.end() # If 'Whole words' is checked, the selection would include the two # non-alphanumeric characters we included in the search, which need # to be removed before marking them. if self.wholeWords.isChecked(): start += 1 end -= 1 self.moveCursor(start, end) else: # We set the cursor to the end if the search was unsuccessful self.parent.paged_text_edit.moveCursor(QTextCursor.End) def replace(self): # Grab the text cursor cursor = self.parent.paged_text_edit.textCursor() # TODO Prevent replace from replacing text not searched for # Check if cursor selection is same as search text # Security if self.lastMatch and cursor.hasSelection(): # We insert the new text, which will override the selected # text cursor.insertText(self.replaceField.toPlainText()) # And set the new cursor self.parent.paged_text_edit.setTextCursor(cursor) def replaceAll(self): # Set lastMatch to None so that the search # starts from the beginning of the document self.lastMatch = None # Initial find() call so that lastMatch is # potentially not None anymore self.find() # Replace and find until find is None again while self.lastMatch: self.replace() self.find() def regexMode(self): # First uncheck the checkboxes self.caseSens.setChecked(False) self.wholeWords.setChecked(False) # Then disable them (gray them out) self.caseSens.setEnabled(False) self.wholeWords.setEnabled(False) def normalMode(self): # Enable checkboxes (un-gray them) self.caseSens.setEnabled(True) self.wholeWords.setEnabled(True) def moveCursor(self, start, end): # We retrieve the QTextCursor object from the parent's QTextEdit cursor = self.parent.paged_text_edit.textCursor() # Then we set the position to the beginning of the last match cursor.setPosition(start) # Next we move the Cursor by over the match and pass the KeepAnchor parameter # which will make the cursor select the the match's text cursor.movePosition(QTextCursor.Right, QTextCursor.KeepAnchor, end - start) # And finally we set this new cursor as the parent's self.parent.paged_text_edit.setTextCursor(cursor)
class RenameOptionsView(QWidget): """View responsible for holding renaming options. Attributes: layout (QVBoxLayout): Main layout of view. frame_layout (QVBoxLayout: Layout of frame which holds options. frame (QFrame): Frame surrounding options. prefix_h_layout (QHBoxLayout): Layout holding prefix options. complete_rename_h_layout (QHBoxLayout): Layout holding complete rename options. search_and_replace_h_layout (QHBoxLayout): Layout holding search and replace options. renumber_h_layout (QHBoxLayout): Layout holding renumber options. remove_ext_h_layout (QHBoxLayout): Layout holding remove options. change_ext_h_layout (QHBoxLayout): Layout holding change extension options. create_backup_h_layout (QHBoxLayout): Layout holding backup options. preview_h_layout (QHBoxLayout): Layout holding preview options. start_lbl (QLabel): Label for renumbering start. padding_lbl (QLabel): Label for renumbering padding. add_prefix_cb (QCheckBox): Used to signify the user wants to add a prefix to the renaming. prefix (QLineEdit): prefix to add. complete_rename_cb (QCheckBox): Used to signify the user wants to completely rename the file. new_name (QLineEdit): New name used when renaming. search_and_replace_cb (QCheckBox): Used to signify the user wants to partially rename files. find (QLineEdit): When searching and replacing this is what the user wants to search for. replace (QLineEdit): When searching and replacing this is what the user wants to replace with. renumber_cb (QCheckBox): Used to signify the user wants to renumber while renaming. start_num (QSpinBox): Number to start with when renumbering files. padding (QComboBox): Padding to apply to renaming when renumbering files. dot_cb (QCheckBox): When checked a dot will be used to separate the renumber from the name. remove_ext_cb (QCheckBox): Used to signify the user wants to remove extensions when renaming. backup_files_cb (QCheckBox): Used to signify the user wants to backup old files before renaming. change_ext_cb (QCheckBox): Used to signify the user wants to change the extension while renaming. change_ext (QLineEdit): New extension to add to the renamed file. preview_cb (QCheckBox): Used to signify the user wants to preview the rename before renaming. """ def __init__(self): super(RenameOptionsView, self).__init__() self.layout = QVBoxLayout() self.frame_layout = QVBoxLayout() self.options_lbl = QLabel(prefs.OPTIONS) self.frame = QFrame() self.prefix_h_layout = QHBoxLayout() self.complete_rename_h_layout = QHBoxLayout() self.search_and_replace_h_layout = QHBoxLayout() self.renumber_h_layout = QHBoxLayout() self.remove_ext_h_layout = QHBoxLayout() self.change_ext_h_layout = QHBoxLayout() self.create_backup_h_layout = QHBoxLayout() self.preview_h_layout = QHBoxLayout() self.start_lbl = QLabel(prefs.START_NUM) self.padding_lbl = QLabel(prefs.PADDING) self.add_prefix_cb = QCheckBox(prefs.PREFIX) self.prefix = QLineEdit(prefs.PREFIX_DEFAULT) self.complete_rename_cb = QCheckBox(prefs.COMPLETE_RENAME) self.new_name = QLineEdit(prefs.COMPLETE_RENAME_DEFAULT) self.search_and_replace_cb = QCheckBox(prefs.SEARCH_AND_REPLACE) self.find = QLineEdit(prefs.SEARCH_AND_REPLACE_DEFAULT) self.replace = QLineEdit(prefs.REPLACE_WITH_DEFAULT) self.renumber_cb = QCheckBox(prefs.RENUMBER) self.start_num = QSpinBox() self.padding = QComboBox() self.dot_cb = QCheckBox(prefs.USE_DOT) self.remove_ext_cb = QCheckBox(prefs.REMOVE_EXT) self.backup_files_cb = QCheckBox(prefs.BACKUP) self.change_ext_cb = QCheckBox(prefs.CHANGE_EXT) self.change_ext = QLineEdit(prefs.CHANGE_EXT_DEFAULT) self.preview_cb = QCheckBox(prefs.PREVIEW) self._configure() def _configure(self) -> None: """Configure the RenameOptionsView.""" self.frame.setLayout(self.frame_layout) self.frame.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken) self.layout.addWidget(self.options_lbl) self.layout.addWidget(self.frame) self.add_prefix_cb.setToolTip(prefs.PREFIX_TOOLTIP) self.prefix.setDisabled(True) self.prefix.setMaximumWidth(prefs.PREFIX_WIDTH) self.prefix.setMinimumWidth(prefs.PREFIX_WIDTH) self.complete_rename_cb.setToolTip(prefs.COMPLETE_RENAME_TOOLTIP) self.new_name.setDisabled(True) self.new_name.setMaximumWidth(prefs.NEW_NAME_WIDTH) self.new_name.setMinimumWidth(prefs.NEW_NAME_WIDTH) self.search_and_replace_cb.setToolTip(prefs.SEARCH_AND_REPLACE_TOOLTIP) self.find.setDisabled(True) self.find.setMinimumWidth(prefs.FIND_WIDTH) self.find.setMaximumWidth(prefs.FIND_WIDTH) self.replace.setDisabled(True) self.replace.setMaximumWidth(prefs.REPLACE_WIDTH) self.replace.setMinimumWidth(prefs.REPLACE_WIDTH) self.renumber_cb.setToolTip(prefs.RENUMBER_TOOLTIP) self.start_num.setToolTip(prefs.START_NUM_TOOLTIP) self.start_num.setDisabled(True) self.start_num.setValue(prefs.START_NUM_DEFAULT) self.start_num.setMinimumWidth(prefs.START_NUM_MIN_WIDTH) self.padding.setToolTip(prefs.PADDING_TOOLTIP) self.padding.setDisabled(True) self.padding.addItems([str(x) for x in range(10)]) self.padding.setCurrentIndex(4) self.padding.setMinimumWidth(prefs.PADDING_MIN_WIDTH) self.dot_cb.setToolTip(prefs.USE_DOT_TOOLTIP) self.dot_cb.setDisabled(True) self.dot_cb.setChecked(True) self.dot_cb.setMinimumWidth(prefs.DOT_WIDTH) self.dot_cb.setMaximumWidth(prefs.DOT_WIDTH) self.remove_ext_cb.setToolTip(prefs.REMOVE_EXT_TOOLTIP) self.change_ext.setToolTip(prefs.CHANGE_EXT_TOOLTIP) self.change_ext.setDisabled(True) self.backup_files_cb.setToolTip(prefs.BACKUP_TOOLTIP) self.backup_files_cb.setChecked(True) self.preview_cb.setToolTip(prefs.PREVIEW_TOOLTIP) self.preview_cb.setChecked(True) self.prefix_h_layout.addWidget(self.add_prefix_cb) self.prefix_h_layout.addWidget(self.prefix) self.frame_layout.addLayout(self.prefix_h_layout) self.complete_rename_h_layout.addWidget(self.complete_rename_cb) self.complete_rename_h_layout.addWidget(self.new_name) self.frame_layout.addLayout(self.complete_rename_h_layout) self.search_and_replace_h_layout.addWidget(self.search_and_replace_cb) self.search_and_replace_h_layout.addWidget(self.find) self.search_and_replace_h_layout.addWidget(self.replace) self.frame_layout.addLayout(self.search_and_replace_h_layout) self.renumber_h_layout.addWidget(self.renumber_cb) self.renumber_h_layout.addStretch(1) self.renumber_h_layout.addWidget(self.start_lbl) self.renumber_h_layout.addWidget(self.start_num) self.renumber_h_layout.addSpacerItem(QSpacerItem(*prefs.SPACER_SIZE)) self.renumber_h_layout.addWidget(self.padding_lbl) self.renumber_h_layout.addWidget(self.padding) self.renumber_h_layout.addSpacerItem(QSpacerItem(*prefs.SPACER_SIZE)) self.renumber_h_layout.addWidget(self.dot_cb) self.frame_layout.addLayout(self.renumber_h_layout) self.change_ext_h_layout.addWidget(self.change_ext_cb) self.change_ext_h_layout.addWidget(self.change_ext) self.frame_layout.addLayout(self.change_ext_h_layout) self.remove_ext_h_layout.addWidget(self.remove_ext_cb) self.frame_layout.addLayout(self.remove_ext_h_layout) self.create_backup_h_layout.addWidget(self.backup_files_cb) self.frame_layout.addLayout(self.create_backup_h_layout) self.preview_h_layout.addWidget(self.preview_cb) self.frame_layout.addLayout(self.preview_h_layout) self.frame_layout.addSpacerItem(QSpacerItem(*prefs.SPACER_SIZE)) self.setLayout(self.layout) def disable_change_ext(self) -> None: """Disable change extension.""" self.change_ext.setDisabled(True) def disable_dot(self) -> None: """Disable dot checkbox.""" self.dot_cb.setDisabled(True) def disable_find(self) -> None: """Disable find.""" self.find.setDisabled(True) def disable_new_name(self) -> None: """Disable new name.""" print("disable new name") self.new_name.setDisabled(True) def disable_padding(self) -> None: """Disable padding.""" self.padding.setDisabled(True) def disable_prefix(self) -> None: """Disable prefix.""" self.prefix.setDisabled(True) def disable_start_num(self) -> None: """Disable start num.""" self.start_num.setDisabled(True) def disable_replace(self) -> None: """Disable replace.""" self.replace.setDisabled(True) def enable_change_ext(self) -> None: """Disable change extension.""" self.change_ext.setDisabled(False) def enable_dot(self) -> None: """Enable dot checkbox.""" self.dot_cb.setEnabled(True) def enable_find(self) -> None: """Enable find.""" self.find.setEnabled(True) def enable_new_name(self) -> None: """Enable new name.""" print("enable new name.") self.new_name.setEnabled(True) def enable_padding(self) -> None: """Enable padding.""" self.padding.setEnabled(True) def enable_prefix(self) -> None: """Enable prefix.""" self.prefix.setEnabled(True) def enable_replace(self) -> None: """Enable replace.""" self.replace.setEnabled(True) def enable_start_num(self) -> None: """Enable start num.""" self.start_num.setEnabled(True) def get_add_prefix(self) -> bool: """Return if end user wants to add a prefix and it is not the default value.""" result = self.get_prefix_checked() if result and self.get_prefix() == prefs.PREFIX_DEFAULT: result = False return result def get_do_backup(self) -> bool: """Return if end user wants to backup files.""" return self.backup_files_cb.isChecked() def get_change_ext(self) -> bool: """Return if the change extension checkbox is checked.""" return self.change_ext_cb.isChecked() def get_do_complete_rename(self) -> bool: """Get if end user wants to completely rename.""" return self.complete_rename_cb.isChecked() def get_dot(self) -> str: """Return dot string based on end users configuration. Note: If the end user has not enable using dot separators an empty string will be returned. """ return "." if self.get_do_dot() else "" def get_do_dot(self) -> bool: """Return if the end user wants to use dot separators when renaming.""" return self.dot_cb.isChecked() def get_do_change_ext(self) -> bool: """Return if the end user wants to change the extension.""" result = self.change_ext_cb.isChecked() if self.get_new_ext() == prefs.CHANGE_EXT_DEFAULT: return False return result def get_do_padding(self) -> bool: """Return if the end user wants to add padding.""" return False if self.get_padding() == 0 else True def get_do_preview(self) -> bool: """Return if the end user wants to preview changes.""" return self.preview_cb.isChecked() def get_do_rename(self) -> bool: """Return if end user wants to rename the full item and it is not the default value.""" result = self.complete_rename_cb.isChecked() if result and self.get_new_name() == prefs.COMPLETE_RENAME_DEFAULT: result = False return result def get_do_renumber(self) -> bool: """Return if the end user wants to renumber.""" return self.renumber_cb.isChecked() def get_do_search(self) -> bool: """Return if end user wants to perform a search and replace AND it is not the default values respectfully. Note: If you only want to know if search and replace is checked use get_search_and_replace. """ result = self.search_and_replace_cb.isChecked() if result and (self.get_find() == prefs.SEARCH_AND_REPLACE_DEFAULT or self.get_replace() == prefs.REPLACE_WITH_DEFAULT): result = False return result def get_do_search_and_replace(self) -> bool: """Return if end user wants to perform a search and replace.""" return self.search_and_replace_cb.isChecked() def get_find(self) -> str: """Return find value.""" return str(self.find.text()) def get_new_ext(self) -> str: """Return new ext.""" return str(self.change_ext.text()) def get_new_name(self) -> str: """Return new_name value.""" return str(self.new_name.text()) def get_padding(self) -> int: """Return the current padding value.""" return int(self.padding.currentText()) def get_prefix_checked(self) -> bool: """Return if the prefix checkbox is checked.""" return self.add_prefix_cb.isChecked() def get_prefix(self) -> str: """Return the current prefix value end user has entered.""" return str(self.prefix.text()) def get_remove_ext(self) -> bool: """Return if end user has checked the remove extension checkbox.""" return self.remove_ext_cb.isChecked() def get_replace(self) -> str: """Return the current replace value end user has entered.""" return str(self.replace.text()) def get_start_num(self) -> int: """Return start number from view.""" return int(self.start_num.value()) def set_change_ext_style(self, style: str) -> None: """Set style of change extension. Args: style: Style sheet applied to change extension. """ self.change_ext.setStyleSheet(style) def set_disabled(self) -> None: """Disable View.""" self.setDisabled(True) def set_enable(self) -> None: """Enable View.""" self.setEnabled(True) def set_find(self, value: str) -> None: """Set the value of find. Args: value: Value applied to find """ self.find.setText(value) def set_find_style(self, style: str) -> None: """Set style of find. Args: style: Style sheet applied to find. """ self.find.setStyleSheet(style) def set_new_name(self, value: str) -> None: """Set the value of new name. Args: value: Value applied to new_name """ self.new_name.setText(value) def set_new_name_style(self, style: str) -> None: """Set style of new_name. Args: style: Style sheet applied to new_name. """ self.new_name.setStyleSheet(style) def set_prefix(self, value: str) -> None: """Set the value of prefix. Args: value: Value applied to prefix """ self.prefix.setText(value) def set_prefix_style(self, style: str) -> None: """Set style of prefix. Args: style: Style sheet applied to prefix. """ self.prefix.setStyleSheet(style) def set_remove_ext(self, state: bool) -> None: """Set the remove_ext checkbox as checked or unchecked. Args: state: Check state of remove_ext. """ self.remove_ext_cb.setCheckState(Qt.Checked if state else Qt.Unchecked) def set_replace(self, value: str) -> None: """Set the value of replace. Args: value: Value applied to replace """ self.replace.setText(value) def set_replace_style(self, style: str) -> None: """Set style of replace. Args: style: Style sheet applied to replace. """ self.replace.setStyleSheet(style)
class HistWidget(ToolWidget): def __init__(self, image, parent=None): super(ToolWidget, self).__init__(parent) self.rgb_radio = QRadioButton(self.tr('RGB')) self.rgb_radio.setChecked(True) self.last_radio = self.rgb_radio self.red_radio = QRadioButton(self.tr('Red')) self.green_radio = QRadioButton(self.tr('Green')) self.blue_radio = QRadioButton(self.tr('Blue')) self.value_radio = QRadioButton(self.tr('Value')) self.log_check = QCheckBox(self.tr('Log scale')) self.grid_check = QCheckBox(self.tr('Show grid')) self.marker_check = QCheckBox(self.tr('Show markers')) self.marker_check.setToolTip( self.tr('Show plot markers for min(--), avg(-), max(-.)')) self.start_slider = ParamSlider([0, 255], 8, 0, label='Start:', bold=True) self.end_slider = ParamSlider([0, 255], 8, 255, label='End:', bold=True) channels = cv.split(cv.cvtColor(image, cv.COLOR_BGR2RGB)) channels.append(cv.cvtColor(image, cv.COLOR_BGR2GRAY)) self.hist = [compute_hist(c) for c in channels] rows, cols, chans = image.shape pixels = rows * cols unique = np.unique(np.reshape(image, (pixels, chans)), axis=0).shape[0] unique_ratio = unique / pixels * 100 unique_label = QLabel( self.tr('total pixels = {}, unique colors = {} ({:.2f}%) '.format( pixels, unique, unique_ratio))) modify_font(unique_label, italic=True) self.rgb_radio.clicked.connect(self.redraw) self.red_radio.clicked.connect(self.redraw) self.green_radio.clicked.connect(self.redraw) self.blue_radio.clicked.connect(self.redraw) self.value_radio.clicked.connect(self.redraw) self.log_check.stateChanged.connect(self.redraw) self.grid_check.stateChanged.connect(self.redraw) self.marker_check.stateChanged.connect(self.redraw) self.start_slider.valueChanged.connect(self.redraw) self.end_slider.valueChanged.connect(self.redraw) self.table_widget = QTableWidget(8, 2) self.table_widget.setHorizontalHeaderLabels( [self.tr('Property'), self.tr('Value')]) self.table_widget.setItem(0, 0, QTableWidgetItem(self.tr('Least frequent'))) self.table_widget.setItem(1, 0, QTableWidgetItem(self.tr('Most frequent'))) self.table_widget.setItem(2, 0, QTableWidgetItem(self.tr('Average level'))) self.table_widget.setItem(3, 0, QTableWidgetItem(self.tr('Median level'))) self.table_widget.setItem(4, 0, QTableWidgetItem(self.tr('Deviation'))) self.table_widget.setItem(5, 0, QTableWidgetItem(self.tr('Pixel count'))) self.table_widget.setItem(6, 0, QTableWidgetItem(self.tr('Percentile'))) self.table_widget.setItem(7, 0, QTableWidgetItem(self.tr('Smoothness'))) for i in range(self.table_widget.rowCount()): modify_font(self.table_widget.item(i, 0), bold=True) self.table_widget.setSelectionMode(QAbstractItemView.SingleSelection) self.table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table_widget.resizeColumnsToContents() self.table_widget.setAlternatingRowColors(True) self.table_widget.setMaximumWidth(200) figure = Figure() plot_canvas = FigureCanvas(figure) # plot_canvas.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) self.axes = plot_canvas.figure.subplots() self.redraw() figure.set_tight_layout(True) right_layout = QVBoxLayout() table_label = QLabel(self.tr('Range properties')) modify_font(table_label, bold=True) table_label.setAlignment(Qt.AlignCenter) right_layout.addWidget(table_label) right_layout.addWidget(self.table_widget) right_layout.addWidget(self.marker_check) right_layout.addWidget(self.start_slider) right_layout.addWidget(self.end_slider) center_layout = QHBoxLayout() center_layout.addWidget(plot_canvas) center_layout.addLayout(right_layout) bottom_layout = QHBoxLayout() bottom_layout.addWidget(self.rgb_radio) bottom_layout.addWidget(self.red_radio) bottom_layout.addWidget(self.green_radio) bottom_layout.addWidget(self.blue_radio) bottom_layout.addWidget(self.value_radio) bottom_layout.addWidget(self.log_check) bottom_layout.addWidget(self.grid_check) bottom_layout.addStretch() bottom_layout.addWidget(unique_label) main_layout = QVBoxLayout() main_layout.addLayout(center_layout) main_layout.addLayout(bottom_layout) self.setLayout(main_layout) def redraw(self): x = np.arange(256) alpha = 0.25 rgb = self.rgb_radio.isChecked() red = self.red_radio.isChecked() green = self.green_radio.isChecked() blue = self.blue_radio.isChecked() value = self.value_radio.isChecked() grid = self.grid_check.isChecked() log = self.log_check.isChecked() try: self.axes.clear() except RecursionError: return y = None if value: y = self.hist[3] self.axes.step(x, y, 'k') self.axes.fill_between(x, y, alpha=alpha, facecolor='k', step='pre') else: # TODO: Ottimizzare facendo un ciclo senza ripetere le istruzioni if red or rgb: y = self.hist[0] self.axes.step(x, y, 'r') self.axes.fill_between(x, y, alpha=alpha, facecolor='r', step='pre') if green or rgb: y = self.hist[1] self.axes.step(x, y, 'g') self.axes.fill_between(x, y, alpha=alpha, facecolor='g', step='pre') if blue or rgb: y = self.hist[2] self.axes.step(x, y, 'b') self.axes.fill_between(x, y, alpha=alpha, facecolor='b', step='pre') if log: self.axes.set_yscale('log') self.axes.set_ylim(bottom=1) else: self.axes.set_yscale('linear') self.axes.set_ylim(bottom=0) self.axes.set_xlim([-1, 256]) self.axes.set_xlabel(self.tr('intensity value')) self.axes.set_ylabel(self.tr('pixel count')) self.axes.set_xticks([0, 64, 128, 192, 255]) self.axes.grid(grid, which='both') if rgb: self.table_widget.setEnabled(False) self.marker_check.setEnabled(False) self.start_slider.setEnabled(False) self.end_slider.setEnabled(False) for i in range(self.table_widget.rowCount()): if self.table_widget.item(i, 1) is not None: self.table_widget.item(i, 1).setText('') self.table_widget.item(i, 1).setBackgroundColor( QColor('white')) else: self.table_widget.setEnabled(True) self.marker_check.setEnabled(True) self.start_slider.setEnabled(True) self.end_slider.setEnabled(True) start = self.start_slider.value() end = self.end_slider.value() if end <= start: end = start + 1 elif start >= end: start = end - 1 total = np.sum(y) x = x[start:end + 1] y = y[start:end + 1] count = np.sum(y) if count != 0: argmin = np.argmin(y) + start argmax = np.argmax(y) + start mean = np.round(np.sum(x * y) / count, 2) stddev = np.round(np.sqrt(np.sum(((x - mean)**2) * y) / count), 2) median = np.argmax(np.cumsum(y) > count / 2) + start percent = np.round(count / total * 100, 2) y = y / np.max(y) sweep = len(y) smooth = 0 if sweep > 2: for i in range(1, sweep - 1): h0 = y[i - 1] h1 = y[i] h2 = y[i + 1] smooth += abs((h0 + h2) / 2 - h1) smooth = np.round((1 - (smooth / (sweep - 2))) * 100, 2) if self.marker_check.isChecked(): self.axes.axvline(argmin, linestyle='--', color='m') self.axes.axvline(mean, linestyle='-', color='m') self.axes.axvline(argmax, linestyle='-.', color='m') else: argmin = argmax = mean = stddev = median = percent = smooth = 0 self.table_widget.setItem(0, 1, QTableWidgetItem(str(argmin))) self.table_widget.setItem(1, 1, QTableWidgetItem(str(argmax))) self.table_widget.setItem(2, 1, QTableWidgetItem(str(mean))) self.table_widget.setItem(3, 1, QTableWidgetItem(str(median))) self.table_widget.setItem(4, 1, QTableWidgetItem(str(stddev))) self.table_widget.setItem(5, 1, QTableWidgetItem(str(count))) self.table_widget.setItem(6, 1, QTableWidgetItem(str(percent) + '%')) self.table_widget.setItem(7, 1, QTableWidgetItem(str(smooth) + '%')) if smooth <= 80: self.table_widget.item(7, 1).setBackgroundColor( QColor.fromHsv(0, 96, 255)) elif smooth <= 90: self.table_widget.item(7, 1).setBackgroundColor( QColor.fromHsv(30, 96, 255)) elif smooth <= 95: self.table_widget.item(7, 1).setBackgroundColor( QColor.fromHsv(60, 96, 255)) else: self.table_widget.item(7, 1).setBackgroundColor( QColor.fromHsv(90, 96, 255)) self.table_widget.resizeColumnsToContents() if start != 0 or end != 255: self.axes.axvline(start, linestyle=':', color='k') self.axes.axvline(end, linestyle=':', color='k') _, top = self.axes.get_ylim() self.axes.fill_between(np.arange(start, end + 1), top, facecolor='y', alpha=alpha * 2) self.axes.figure.canvas.draw()
class ChannelArithmeticDialog(ieb.ImarisExtensionBase): """ Channel Arithmetic and Beyond ============================= `View on GitHub <https://github.com/niaid/imaris_extensions>`_ This program enables one to specify arithmetic expressions which are used to create new channels. The basic arithmetic operations are supported: +,-,*,/,**. More advanced operations that run short `SimpleITK <https://simpleitk.org/>`_ code snippets are also supported. Channels are referenced using square brackets and the channel index, starting at **zero**. To apply an expression to all channels, use the channel index 'i'. When creating a single new channel, the arithmetic expression consists of literal channel numbers, one can select a name and color for the new channel. When creating multiple new channels, the arithmetic expression is applied to all channels, the postfix '_modified' is appended to the original channel names and the original color is copied over. Note that for all channels created by the program the channel description will include the arithmetic expression used to create that channel. This transparently supports your efforts to conduct reproducible research. Because an Imaris image has a specific pixel type (8, 16, 32 bit unsigned integer and 32 bit floating point) all computations are performed using a 32 bit floating point representation and then clamped to the range of the image's pixel type. The program allows you to use the same expression on multiple files. In this case literal channel values are limited by the number of shared channels. Thus, if working with two files one with three channels and one with four channels, the valid literal channel values are limited to 0, 1, 2. We cannot use 3 as it does not exist in all files. On the other hand, if our autofluorescence channel is one of these channels, e.g. channel 0, we can subtract it from all channels in both files, `[i]-[0]`. Basic Examples -------------- Multiply channels zero and three: .. code-block:: Python [0]*[3] Multiply channels zero and three and subtract the result from channel four: .. code-block:: Python [4] - ([0]*[3]) Duplicate all channels: .. code-block:: Python [i] Subtract channel zero from all channels: .. code-block:: Python [i]-[0] Advanced Examples ----------------- Threshold channel one using a value of 100, resulting image is binary values in {0,1}: .. code-block:: Python [1]>100 Threshold a specific channel to create a binary result using the Otsu filter: .. code-block:: Python sitk.OtsuThreshold([1], 0, 1) Threshold a specific channel retaining the values above the threshold: .. code-block:: Python sitk.Cast([1]>100, sitk.sitkFloat32)*[1] Threshold a specific channel, get all connected components, then sort the components according to size, discarding those smaller than a minimum size and create a binary mask corresponding to the largest component, which is the first label(second largest component label is 2 etc.) .. code-block:: Python sitk.RelabelComponent(sitk.ConnectedComponent([1]>100), minimumObjectSize = 50)==1 Create a binary mask representing the colocalization of two channels, intensity values below 20 are considred noise: .. code-block:: Python ([1]>20)*([2]>20) Create a binary mask representing the colocalization of two channels. We are interested in all pixels in channel 2 that have a value above 20 and that are less than 1.0um away from pixels in channel 1 that have a value above 100 (**note**: this operation yields different results when run using a slice-by-slice approach vs. a volumetric approach): .. code-block:: Python (sitk.Cast([2]>20, sitk.sitkFloat32) * sitk.Abs(sitk.SignedMaurerDistanceMap([1]>100, insideIsPositive=False, squaredDistance=False, useImageSpacing=True)))<=1.0 Create a binary mask using thresholding and then perform morphological closing (dilation followed by erosion) with distance maps, useful for filling holes: .. code-block:: Python sitk.SignedMaurerDistanceMap(sitk.SignedMaurerDistanceMap([1]>100, insideIsPositive=False, squaredDistance=False, useImageSpacing=True) < 1.0, insideIsPositive=False, squaredDistance=False, useImageSpacing=True)<-1.0 Create a binary mask using thresholding and then perform morphological opening (erosion followed by dilation) with distance maps, useful for removing small islands: .. code-block:: Python sitk.SignedMaurerDistanceMap(sitk.SignedMaurerDistanceMap([1]>100, insideIsPositive=False, squaredDistance=False, useImageSpacing=True) < -0.2, insideIsPositive=False, squaredDistance=False, useImageSpacing=True)<0.2 """ # noqa def __init__(self): super(ChannelArithmeticDialog, self).__init__() # Channel indexes in the arithmetic calculator are denoted using a # regular expression: one or more digits in square brackets (e.g. [1234]). # First digit is zero and nothing afterwards or first digit is in [1-9] and # there are possibly more digits afterwards. # Starting index is zero. self.channel_pattern = re.compile(r"\[(0|[1-9]\d*)\]") # Use QT's global threadpool, documentation says: "This global thread pool # automatically maintains an optimal number of threads based on the # number of cores in the CPU." self.threadpool = QThreadPool.globalInstance() # Configure the help dialog. self.help_dialog = HelpDialog(w=700, h=500) self.help_dialog.setWindowTitle("Channel Arithmetic Help") self.help_dialog.set_rst_text( inspect.getdoc(self), pygments_css_file_name="pygments_dark.css") self.__create_gui() self.setWindowTitle("Channel Arithmetic") self.processing_error = False self.show() def __create_gui(self): menu_bar = self.menuBar() # Force menubar to be displayed in the application on OSX/Linux, otherwise it # is displayed in the system menubar menu_bar.setNativeMenuBar(False) self.help_button = QPushButton("Help") self.help_button.clicked.connect(self.help_dialog.show) menu_bar.setCornerWidget(self.help_button, Qt.TopLeftCorner) central_widget = QWidget(self) gui_layout = QVBoxLayout() central_widget.setLayout(gui_layout) self.setCentralWidget(central_widget) select_files_widget = self.__create_select_files_widget() arithmetic_widget = self.__create_arithmetic_widget() self.stack = QStackedWidget(self) self.stack.addWidget(select_files_widget) self.stack.addWidget(arithmetic_widget) gui_layout.addWidget(self.stack) self.status_bar = self.statusBar() def closeEvent(self, event): """ Override the closeEvent method so that clicking the 'x' button also closes all of the dialogs. """ self.help_dialog.close() event.accept() def __create_arithmetic_widget(self): wid = QWidget(self) arithmetic_layout = QVBoxLayout() wid.setLayout(arithmetic_layout) self.valid_indexes_label = QLabel("") arithmetic_layout.addWidget(self.valid_indexes_label) layout = QHBoxLayout() layout.setAlignment(Qt.AlignLeft) layout.addWidget(QLabel("Enter new channel arithmetic expression:")) arithmetic_layout.addLayout(layout) self.arithmetic_expression_text_edit = QTextEdit() arithmetic_layout.addWidget(self.arithmetic_expression_text_edit) self.slice_by_slice_checkbox = QCheckBox( "Slice by slice (smaller memory footprint).") arithmetic_layout.addWidget(self.slice_by_slice_checkbox) layout = QHBoxLayout() layout.addWidget(QLabel("New channel name:")) self.new_channel_name_line_edit = QLineEdit() layout.addWidget(self.new_channel_name_line_edit) arithmetic_layout.addLayout(layout) layout = QHBoxLayout() layout.addWidget(QLabel("New channel color:")) self.new_channel_color_button = QPushButton() self.new_channel_color_button.clicked.connect( self.__select_color_callback) layout.addWidget(self.new_channel_color_button) arithmetic_layout.addLayout(layout) self.apply_button = QPushButton("Apply") self.apply_button.clicked.connect(self.__channel_arithmetic_wrapper) arithmetic_layout.addWidget(self.apply_button) progress_wid = QWidget() self.progress_grid_layout = QGridLayout() progress_wid.setLayout(self.progress_grid_layout) scroll_area = QScrollArea() scroll_area.setWidget(progress_wid) scroll_area.setWidgetResizable(True) arithmetic_layout.addWidget(scroll_area) layout = QHBoxLayout() layout.setAlignment(Qt.AlignLeft) self.processing_prev_button = QPushButton("Prev") self.processing_prev_button.clicked.connect( lambda: self.stack.setCurrentIndex(0)) layout.addWidget(self.processing_prev_button) arithmetic_layout.addLayout(layout) return wid def __configure_and_show_arithmetic_widget(self): file_names = self.input_files_edit.toPlainText().split("\n") num_channels = [] problematic_images = [] for file_name in file_names: try: meta_data = sio.read_metadata(file_name) num_channels.append(len(meta_data["channels_information"])) except Exception: problematic_images.append(file_name) if problematic_images: self._error_function( "Problem encountered reading the following file(s):\n" + "\n".join(problematic_images)) return self.max_channel_index = min(num_channels) - 1 self.valid_indexes_label.setText( f"Valid channel indexes: 0...{self.max_channel_index}, i") self.arithmetic_expression_text_edit.clear() self.slice_by_slice_checkbox.setChecked(False) self.new_channel_name_line_edit.clear() # Remove all widgets from layout, done in reverse order because # removing from the begining shifts the rest of the items for i in reversed(range(self.progress_grid_layout.count())): self.progress_grid_layout.itemAt(i).widget().setParent(None) for i, file_name in enumerate(file_names): self.progress_grid_layout.addWidget( QLabel(os.path.basename(file_name)), i, 0) progress_bar = QProgressBar() progress_bar.setMaximum(100) self.progress_grid_layout.addWidget(progress_bar, i, 1) self.stack.setCurrentIndex(1) def __create_select_files_widget(self): wid = QWidget() input_layout = QVBoxLayout() wid.setLayout(input_layout) layout = QHBoxLayout() layout.addWidget(QLabel("File names:")) layout.setAlignment(Qt.AlignLeft) button = QPushButton("Browse") button.setToolTip("Select input files for arithmetic operation.") button.clicked.connect(self.__browse_select_input_callback) layout.addWidget(button) input_layout.addLayout(layout) self.input_files_edit = QTextEdit() self.input_files_edit.setReadOnly(True) input_layout.addWidget(self.input_files_edit) layout = QHBoxLayout() layout.setAlignment(Qt.AlignRight) self.input_files_next_button = QPushButton("Next") self.input_files_next_button.setEnabled(False) self.input_files_next_button.clicked.connect( self.__configure_and_show_arithmetic_widget) layout.addWidget(self.input_files_next_button) input_layout.addLayout(layout) return wid def __browse_select_input_callback(self): file_names, _ = QFileDialog.getOpenFileNames( self, "QFileDialog.getOpenFileNames()", "", "Imaris Images (*.ims);;All Files (*)", ) if file_names: self.input_files_edit.setText("\n".join(file_names)) self.input_files_next_button.setEnabled(True) def __select_color_callback(self): color = QColorDialog.getColor() if color.isValid(): self.new_channel_color_button.setStyleSheet( f"background-color :rgb({color.red()},{color.green()},{color.blue()})" ) def __channel_arithmetic_wrapper(self): # Get the arithmetic expression after removing all whitespace arithmetic_expression = "".join( self.arithmetic_expression_text_edit.toPlainText().split()) color = self.new_channel_color_button.palette().button().color() if arithmetic_expression: # Get the explicit channel indexes that appear in the expression and # check that they are in the valid range. channel_indexes = re.findall(self.channel_pattern, arithmetic_expression) invalid_channels = [ ci for ci in channel_indexes if int(ci) not in range(self.max_channel_index + 1) ] if invalid_channels: self._error_function( "The following channels specified in the arithmetic expression" + f" are outside the valid range [0,{self.max_channel_index}]: " + ", ".join(invalid_channels)) return # Disable the UI interaction during computation self.arithmetic_expression_text_edit.setReadOnly(True) self.slice_by_slice_checkbox.setEnabled(False) self.new_channel_name_line_edit.setReadOnly(True) self.new_channel_color_button.setEnabled(False) self.apply_button.setEnabled(False) self.processing_prev_button.setEnabled(False) QApplication.setOverrideCursor(Qt.WaitCursor) file_names = self.input_files_edit.toPlainText().split("\n") self.num_threads_left = len(file_names) for i, input_file_name in enumerate(file_names): # Configure and perform computation in another thread. arithmetic_calculator = ArithmeticCalculator( self.channel_pattern) arithmetic_calculator.signals.finished.connect( self.__arithmetic_finished) arithmetic_calculator.signals.processing_error.connect( self._processing_error_function) arithmetic_calculator.signals.progress_signal.connect( self.progress_grid_layout.itemAtPosition( i, 1).widget().setValue) arithmetic_calculator.signals.update_state_signal.connect( self.status_bar.showMessage) arithmetic_calculator.input_file_name = input_file_name arithmetic_calculator.arithmetic_expression = arithmetic_expression arithmetic_calculator.new_channel_color = [ color.red() / 255.0, color.green() / 255.0, color.blue() / 255.0, ] arithmetic_calculator.new_channel_alpha = color.alpha() / 255.0 arithmetic_calculator.new_channel_name = ( self.new_channel_name_line_edit.text().strip()) arithmetic_calculator.slice_by_slice = ( self.slice_by_slice_checkbox.isChecked()) self.threadpool.start(arithmetic_calculator) else: self._error_function( "No action taken: arithmetic expression not set.") def __arithmetic_finished(self): self.num_threads_left = self.num_threads_left - 1 if self.num_threads_left == 0: QApplication.restoreOverrideCursor() self.status_bar.clearMessage() for i in range(self.progress_grid_layout.rowCount()): self.progress_grid_layout.itemAtPosition( i, 1).widget().setValue(0) # Enable the UI interaction after computation self.arithmetic_expression_text_edit.setReadOnly(False) self.slice_by_slice_checkbox.setEnabled(True) self.new_channel_name_line_edit.setReadOnly(False) self.new_channel_color_button.setEnabled(True) self.apply_button.setEnabled(True) self.processing_prev_button.setEnabled(True) # Inform the user that the calculations completed. If processing errors # occured then the desired operation may not have happened, but the # calculation was completed. QMessageBox().information(self, "Message", "Calculation completed.") self.processing_error = False
class AudioInfoDialog(QDialog): def __init__(self, audios_name, audios_delay, audios_language, audios_track_name, audios_set_default, audios_set_forced, audios_default_value_delay, audios_default_value_language, audios_default_value_track_name, audios_default_value_set_default, audios_default_value_set_forced, audio_set_default_disabled=False, audio_set_forced_disabled=False, disable_edit=False, parent=None): super().__init__(parent) self.window_title = "Audio Info" self.state = "no" self.audios_count = len(audios_delay) self.messageIcon = QLabel() self.audio_tab_comboBox = InfoCellDialogTabComboBox( hint="Audios Groups") for i in range(self.audios_count): self.audio_tab_comboBox.addItem("Audio #" + str(i + 1)) self.audio_tab_comboBox.setCurrentIndex(0) self.audio_tab_comboBox.currentIndexChanged.connect( self.update_current_audio_index) self.current_audio_index = 0 self.disable_edit = disable_edit self.current_audio_name = audios_name self.current_audio_language = audios_language self.current_audio_delay = audios_delay self.current_audio_track_name = audios_track_name self.current_audio_set_default = audios_set_default self.current_audio_set_forced = audios_set_forced self.default_audio_language = audios_default_value_language self.default_audio_delay = audios_default_value_delay self.default_audio_track_name = audios_default_value_track_name self.default_audio_set_default = audios_default_value_set_default self.default_audio_set_forced = audios_default_value_set_forced self.audio_set_default_disabled = audio_set_default_disabled self.audio_set_forced_disabled = audio_set_forced_disabled self.audio_name_label = QLabel("Audio Name:") self.audio_name_value = QLabel( str(self.current_audio_name[self.current_audio_index])) width_to_be_fixed = 0 for i in range(len(self.current_audio_name)): width_to_be_fixed = max( width_to_be_fixed, self.audio_name_value.fontMetrics().boundingRect( self.current_audio_name[i]).width()) self.audio_name_value.setFixedWidth(width_to_be_fixed + 10) self.audio_delay_label = QLabel("Audio Delay:") self.audio_delay_spin = QDoubleSpinBox() self.setup_audio_delay_spin() self.audio_language_label = QLabel("Audio Language:") self.audio_language_comboBox = QComboBox() self.setup_audio_language_comboBox() self.audio_track_name_label = QLabel("Audio Track Name:") self.audio_track_name_lineEdit = QLineEdit() self.setup_audio_track_name_lineEdit() self.audio_set_forced_label = QLabel("Audio Forced State:") self.audio_set_forced_checkBox = QCheckBox() self.setup_audio_set_forced_checkBox() self.audio_set_default_label = QLabel("Audio Default State:") self.audio_set_default_checkBox = QCheckBox() self.setup_audio_set_default_checkBox() self.yes_button = QPushButton("OK") self.no_button = QPushButton("Cancel") self.reset_button = QPushButton("Reset To Default") self.buttons_layout = QHBoxLayout() self.audio_delay_layout = QHBoxLayout() self.audio_language_layout = QHBoxLayout() self.audio_track_name_layout = QHBoxLayout() self.audio_set_default_layout = QHBoxLayout() self.audio_set_forced_layout = QHBoxLayout() self.buttons_layout.addWidget(QLabel(""), stretch=3) self.buttons_layout.addWidget(self.reset_button, stretch=2) self.buttons_layout.addWidget(self.yes_button, stretch=2) self.buttons_layout.addWidget(self.no_button, stretch=2) self.buttons_layout.addWidget(QLabel(""), stretch=3) self.audio_setting_layout = QGridLayout() self.audio_editable_setting_layout = QFormLayout() self.audio_editable_setting_layout.addRow(self.audio_name_label, self.audio_name_value) self.audio_editable_setting_layout.addRow( self.audio_track_name_label, self.audio_track_name_lineEdit) self.audio_editable_setting_layout.addRow(self.audio_language_label, self.audio_language_comboBox) self.audio_editable_setting_layout.addRow(self.audio_delay_label, self.audio_delay_spin) self.audio_editable_setting_layout.addRow( self.audio_set_default_label, self.audio_set_default_checkBox) self.audio_editable_setting_layout.addRow( self.audio_set_forced_label, self.audio_set_forced_checkBox) self.audio_setting_layout.addWidget(self.audio_tab_comboBox, 0, 0) self.audio_setting_layout.addLayout(self.audio_editable_setting_layout, 1, 0, 5, 2) self.audio_setting_layout.addWidget(self.messageIcon, 1, 3, 5, -1) self.main_layout = QGridLayout() self.main_layout.addLayout(self.audio_setting_layout, 0, 0, 2, 3) self.main_layout.addLayout(self.buttons_layout, 2, 0, 1, -1) self.main_layout.setContentsMargins(20, 20, 20, 20) self.setLayout(self.main_layout) self.setup_ui() self.signal_connect() def setup_ui(self): self.disable_question_mark_window() self.messageIcon.setPixmap( QtGui.QPixmap(GlobalFiles.AudioIconPath).scaledToHeight(100)) self.set_dialog_values() self.set_default_buttons() if self.audio_set_default_disabled: self.audio_set_default_disable() if self.audio_set_forced_disabled: self.audio_set_forced_disable() if self.disable_edit: self.audio_track_name_lineEdit.setEnabled(False) self.audio_language_comboBox.setEnabled(False) self.audio_delay_spin.setEnabled(False) self.audio_set_default_checkBox.setEnabled(False) self.audio_set_forced_checkBox.setEnabled(False) self.reset_button.setEnabled(False) self.setup_tool_tip_hint_audio_set_default() self.setup_tool_tip_hint_audio_set_forced() def signal_connect(self): self.audio_track_name_lineEdit.textEdited.connect( self.update_current_audio_track_name) self.audio_delay_spin.editingFinished.connect( self.update_current_audio_delay) self.audio_language_comboBox.currentTextChanged.connect( self.update_current_audio_language) self.audio_set_default_checkBox.stateChanged.connect( self.update_current_audio_set_default) self.audio_set_forced_checkBox.stateChanged.connect( self.update_current_audio_set_forced) self.yes_button.clicked.connect(self.click_yes) self.no_button.clicked.connect(self.click_no) self.reset_button.clicked.connect(self.reset_audio_setting) def click_yes(self): self.state = "yes" self.close() def click_no(self): self.state = "no" self.close() def set_dialog_values(self): self.setWindowTitle(self.window_title) self.setWindowIcon(GlobalFiles.InfoSettingIcon) def disable_question_mark_window(self): self.setWindowFlag(Qt.WindowContextHelpButtonHint, on=False) def increase_message_font_size(self, value): message_font = self.message.font() message_font.setPointSize(self.message.fontInfo().pointSize() + value) self.message.setFont(message_font) def set_default_buttons(self): self.yes_button.setDefault(True) self.yes_button.setFocus() def showEvent(self, a0: QtGui.QShowEvent) -> None: super().showEvent(a0) self.setFixedSize(self.size()) def setup_audio_track_name_lineEdit(self): self.audio_track_name_lineEdit.setClearButtonEnabled(True) self.audio_track_name_lineEdit.setText( self.current_audio_track_name[self.current_audio_index]) def setup_audio_language_comboBox(self): self.audio_language_comboBox.addItems(AllAudiosLanguages) self.audio_language_comboBox.setCurrentIndex( AllAudiosLanguages.index( self.current_audio_language[self.current_audio_index])) self.audio_language_comboBox.setMaxVisibleItems(8) self.audio_language_comboBox.setStyleSheet( "QComboBox { combobox-popup: 0; }") def setup_audio_delay_spin(self): # self.audio_delay_spin.setMaximumWidth(screen_size.width() // 16) self.audio_delay_spin.setDecimals(3) self.audio_delay_spin.setMinimum(-9999.0) self.audio_delay_spin.setMaximum(9999.0) self.audio_delay_spin.setSingleStep(0.5) self.audio_delay_spin.setValue( float(self.current_audio_delay[self.current_audio_index])) def setup_audio_set_default_checkBox(self): self.audio_set_default_checkBox.setText("Set Default") self.audio_set_default_checkBox.setChecked( bool(self.current_audio_set_default[self.current_audio_index])) def setup_audio_set_forced_checkBox(self): self.audio_set_forced_checkBox.setText("Set Forced") self.audio_set_forced_checkBox.setChecked( bool(self.current_audio_set_forced[self.current_audio_index])) def update_current_audio_track_name(self): self.current_audio_track_name[self.current_audio_index] = str( self.audio_track_name_lineEdit.text()) def update_current_audio_delay(self): self.current_audio_delay[self.current_audio_index] = round( self.audio_delay_spin.value(), 5) def update_current_audio_language(self): self.current_audio_language[self.current_audio_index] = str( self.audio_language_comboBox.currentText()) def update_current_audio_set_default(self): new_state = self.audio_set_default_checkBox.checkState() == Qt.Checked self.current_audio_set_default[self.current_audio_index] = new_state if new_state: for i in range(len(self.current_audio_set_default)): if i != self.current_audio_index: self.current_audio_set_default[i] = False def update_current_audio_set_forced(self): new_state = self.audio_set_forced_checkBox.checkState() == Qt.Checked self.current_audio_set_forced[self.current_audio_index] = new_state if new_state: for i in range(len(self.current_audio_set_forced)): if i != self.current_audio_index: self.current_audio_set_forced[i] = False def reset_audio_setting(self): self.current_audio_language[ self.current_audio_index] = self.default_audio_language[ self.current_audio_index] self.current_audio_delay[ self.current_audio_index] = self.default_audio_delay[ self.current_audio_index] self.current_audio_track_name[ self.current_audio_index] = self.default_audio_track_name[ self.current_audio_index] self.current_audio_set_default[ self.current_audio_index] = self.default_audio_set_default[ self.current_audio_index] self.current_audio_set_forced[ self.current_audio_index] = self.default_audio_set_forced[ self.current_audio_index] self.audio_language_comboBox.setCurrentIndex( AllAudiosLanguages.index( self.current_audio_language[self.current_audio_index])) self.audio_delay_spin.setValue( float(self.current_audio_delay[self.current_audio_index])) self.audio_track_name_lineEdit.setText( self.current_audio_track_name[self.current_audio_index]) self.audio_set_default_checkBox.setChecked( bool(self.current_audio_set_default[self.current_audio_index])) self.audio_set_forced_checkBox.setChecked( bool(self.current_audio_set_forced[self.current_audio_index])) def audio_set_default_disable(self): self.audio_set_default_checkBox.setDisabled(True) def audio_set_forced_disable(self): self.audio_set_forced_checkBox.setDisabled(True) def setup_tool_tip_hint_audio_set_default(self): if self.audio_set_default_checkBox.isEnabled(): self.audio_set_default_checkBox.setToolTip( "<nobr>set this audio to be the default audio track " "when play") self.audio_set_default_checkBox.setToolTipDuration(12000) else: self.audio_set_default_checkBox.setToolTip( "<nobr>set this audio to be the default audio track when play<br><b>Disabled</b> because " "option " "<b>make this audio default</b> is enabled on mux setting tab " ) self.audio_set_default_checkBox.setToolTipDuration(12000) def setup_tool_tip_hint_audio_set_forced(self): if self.audio_set_forced_checkBox.isEnabled(): self.audio_set_forced_checkBox.setToolTip( "<nobr>set this audio to be the forced audio track when " "play") self.audio_set_forced_checkBox.setToolTipDuration(12000) else: self.audio_set_forced_checkBox.setToolTip( "<nobr>set this audio to be the forced audio track when play<br><b>Disabled</b> because " "option " "<b>make this audio default and forced</b> is enabled on mux setting tab " ) self.audio_set_forced_checkBox.setToolTipDuration(12000) def update_current_audio_index(self, new_index): self.current_audio_index = new_index self.audio_delay_spin.setValue( float(self.current_audio_delay[self.current_audio_index])) self.audio_set_default_checkBox.setChecked( bool(self.current_audio_set_default[self.current_audio_index])) self.audio_set_forced_checkBox.setChecked( bool(self.current_audio_set_forced[self.current_audio_index])) self.audio_language_comboBox.setCurrentIndex( AllAudiosLanguages.index( self.current_audio_language[self.current_audio_index])) self.audio_track_name_lineEdit.setText( self.current_audio_track_name[self.current_audio_index]) self.audio_name_value.setText( str(self.current_audio_name[self.current_audio_index])) def execute(self): self.exec_()
class PlotsWidget(ToolWidget): def __init__(self, image, parent=None): super(PlotsWidget, self).__init__(parent) choices = ['Red', 'Green', 'Blue', 'Hue', 'Saturation', 'Value'] self.xaxis_combo = QComboBox() self.xaxis_combo.addItems(choices) self.xaxis_combo.setCurrentIndex(3) self.yaxis_combo = QComboBox() self.yaxis_combo.addItems(choices) self.yaxis_combo.setCurrentIndex(4) self.zaxis_combo = QComboBox() self.zaxis_combo.addItems(choices) self.zaxis_combo.setCurrentIndex(5) self.sampling_spin = QSpinBox() levels = int(np.log2(min(image.shape[:-1]))) self.sampling_spin.setRange(0, levels) self.sampling_spin.setSpecialValueText(self.tr('Off')) # self.sampling_spin.setSuffix(self.tr(' level(s)')) self.sampling_spin.setValue(1) self.size_spin = QSpinBox() self.size_spin.setRange(1, 10) self.size_spin.setValue(1) self.size_spin.setSuffix(self.tr(' pt')) self.style_combo = QComboBox() self.markers = [',', '.', 'o', '8', 's', 'p', 'P', '*', 'h', 'H', 'X', 'D'] self.style_combo.addItems( ['pixel', 'point', 'circle', 'octa', 'square', 'penta', 'plus', 'star', 'hexa1', 'hexa2', 'cross', 'diamond']) self.alpha_spin = QDoubleSpinBox() self.alpha_spin.setRange(0, 1) self.alpha_spin.setDecimals(2) self.alpha_spin.setSingleStep(0.05) self.alpha_spin.setValue(1) self.colors_check = QCheckBox(self.tr('Show colors')) self.grid_check = QCheckBox(self.tr('Show grid')) self.norm_check = QCheckBox(self.tr('Normalized')) self.total_label = QLabel() img = np.copy(image) self.colors = [None] * (levels + 1) for scale in range(levels + 1): rgb = cv.cvtColor(img.astype(np.float32) / 255, cv.COLOR_BGR2RGB) hsv = cv.cvtColor(rgb, cv.COLOR_RGB2HSV) hsv[:, :, 0] /= 360 shape = (img.shape[0] * img.shape[1], img.shape[2]) self.colors[scale] = np.concatenate((np.reshape(rgb, shape), np.reshape(hsv, shape)), axis=1) img = cv.pyrDown(img) figure2 = Figure() plot2_canvas = FigureCanvas(figure2) self.axes2 = plot2_canvas.figure.subplots() toolbar2 = NavigationToolbar(plot2_canvas, self) plot2_layout = QVBoxLayout() plot2_layout.addWidget(plot2_canvas) plot2_layout.addWidget(toolbar2) plot2_widget = QWidget() plot2_widget.setLayout(plot2_layout) figure3 = Figure() plot3_canvas = FigureCanvas(figure3) self.axes3 = plot3_canvas.figure.add_subplot(111, projection='3d') toolbar3 = NavigationToolbar(plot3_canvas, self) plot3_layout = QVBoxLayout() plot3_layout.addWidget(plot3_canvas) plot3_layout.addWidget(toolbar3) plot3_widget = QWidget() plot3_widget.setLayout(plot3_layout) self.tab_widget = QTabWidget() self.tab_widget.addTab(plot2_widget, '2D Plot') self.tab_widget.addTab(plot3_widget, '3D Plot') self.redraw() figure2.set_tight_layout(True) figure3.set_tight_layout(True) self.xaxis_combo.currentIndexChanged.connect(self.redraw) self.yaxis_combo.currentIndexChanged.connect(self.redraw) self.zaxis_combo.currentIndexChanged.connect(self.redraw) self.sampling_spin.valueChanged.connect(self.redraw) self.size_spin.valueChanged.connect(self.redraw) self.style_combo.currentIndexChanged.connect(self.redraw) self.alpha_spin.valueChanged.connect(self.redraw) self.colors_check.stateChanged.connect(self.redraw) self.grid_check.stateChanged.connect(self.redraw) self.norm_check.stateChanged.connect(self.redraw) self.tab_widget.currentChanged.connect(self.redraw) params_layout = QGridLayout() params_layout.addWidget(QLabel(self.tr('X axis:')), 0, 0) params_layout.addWidget(self.xaxis_combo, 0, 1) params_layout.addWidget(QLabel(self.tr('Y axis:')), 1, 0) params_layout.addWidget(self.yaxis_combo, 1, 1) params_layout.addWidget(QLabel(self.tr('Z axis:')), 2, 0) params_layout.addWidget(self.zaxis_combo, 2, 1) params_layout.addWidget(QLabel(self.tr('Subsampling:')), 0, 2) params_layout.addWidget(self.sampling_spin, 0, 3) params_layout.addWidget(QLabel(self.tr('Point size:')), 1, 2) params_layout.addWidget(self.size_spin, 1, 3) # params_layout.addWidget(QLabel(self.tr('Point style:')), 2, 2) # params_layout.addWidget(self.style_combo, 3, 4) params_layout.addWidget(QLabel(self.tr('Point alpha:')), 2, 2) params_layout.addWidget(self.alpha_spin, 2, 3) params_layout.addWidget(self.colors_check, 0, 4) params_layout.addWidget(self.grid_check, 1, 4) params_layout.addWidget(self.total_label, 2, 4) bottom_layout = QHBoxLayout() bottom_layout.addLayout(params_layout) bottom_layout.addStretch() main_layout = QVBoxLayout() main_layout.addWidget(self.tab_widget) main_layout.addLayout(bottom_layout) self.setLayout(main_layout) def redraw(self): start = time() v = self.sampling_spin.value() x = self.colors[v][:, self.xaxis_combo.currentIndex()] y = self.colors[v][:, self.yaxis_combo.currentIndex()] s = self.size_spin.value()**2 c = None if not self.colors_check.isChecked() else self.colors[v][:, :3] if self.tab_widget.currentIndex() == 0: self.zaxis_combo.setEnabled(False) self.grid_check.setEnabled(True) self.alpha_spin.setEnabled(True) a = self.alpha_spin.value() xlim = self.axes2.get_xlim() ylim = self.axes2.get_ylim() self.axes2.clear() self.axes2.set_facecolor([0.5] * 3 if c is not None else [1.0] * 3) self.axes2.scatter(x, y, s, c, '.', alpha=a) self.axes2.set_xlabel(self.xaxis_combo.currentText()) self.axes2.set_ylabel(self.yaxis_combo.currentText()) self.axes2.grid(self.grid_check.isChecked(), which='both') self.axes2.set_xlim(xlim) self.axes2.set_ylim(ylim) self.axes2.figure.canvas.draw() else: self.zaxis_combo.setEnabled(True) self.grid_check.setEnabled(False) self.alpha_spin.setEnabled(False) z = self.colors[v][:, self.zaxis_combo.currentIndex()] self.axes3.clear() self.axes3.set_facecolor([0.5] * 3 if c is not None else [1.0] * 3) self.axes3.scatter(x, y, z, s=s, c=c, marker='.', depthshade=True) self.axes3.set_xlabel(self.xaxis_combo.currentText()) self.axes3.set_ylabel(self.yaxis_combo.currentText()) self.axes3.set_zlabel(self.zaxis_combo.currentText()) self.axes3.grid(self.grid_check.isChecked(), which='both') self.axes3.figure.canvas.draw() self.total_label.setText(self.tr('[{} points]'.format(len(x)))) self.info_message.emit('Plot redraw = {}'.format(elapsed_time(start)))
class ComparisonWidget(ToolWidget): def __init__(self, image, parent=None): super(ComparisonWidget, self).__init__(parent) load_button = QPushButton(self.tr('Load reference...')) self.file_label = QLabel() modify_font(self.file_label, bold=True) self.comp_label = QLabel(self.tr('Comparison:')) self.normal_radio = QRadioButton(self.tr('Normal')) self.normal_radio.setChecked(True) self.diff_radio = QRadioButton(self.tr('Difference')) self.gray_check = QCheckBox(self.tr('Grayscale')) self.equal_check = QCheckBox(self.tr('Equalized')) self.map_radio = QRadioButton(self.tr('SSIM Map')) self.last_radio = self.normal_radio self.comp_label.setEnabled(False) self.normal_radio.setEnabled(False) self.diff_radio.setEnabled(False) self.gray_check.setEnabled(False) self.equal_check.setEnabled(False) self.map_radio.setEnabled(False) self.evidence = image self.reference = np.full_like(image, 127) self.difference = np.full_like(image, 127) self.equalized = np.full_like(image, 127) self.indexmap = np.full_like(image, 127) self.evidence_viewer = ImageViewer(self.evidence, None, self.tr('Evidence')) self.reference_viewer = ImageViewer(self.reference, None, self.tr('Reference')) self.table_widget = QTableWidget(5, 2) self.table_widget.setHorizontalHeaderLabels( [self.tr('Index'), self.tr('Value')]) self.table_widget.setItem(0, 0, QTableWidgetItem(self.tr('MSE'))) self.table_widget.setItem(1, 0, QTableWidgetItem(self.tr('COVAR'))) self.table_widget.setItem(2, 0, QTableWidgetItem(self.tr('PSNR'))) self.table_widget.setItem(3, 0, QTableWidgetItem(self.tr('SSIM'))) self.table_widget.setItem(4, 0, QTableWidgetItem(self.tr('CORR'))) for i in range(self.table_widget.rowCount()): modify_font(self.table_widget.item(i, 0), bold=True) self.table_widget.setEnabled(False) load_button.clicked.connect(self.load) self.normal_radio.toggled.connect(self.change) self.diff_radio.toggled.connect(self.change) self.gray_check.stateChanged.connect(self.change) self.equal_check.stateChanged.connect(self.change) self.map_radio.toggled.connect(self.change) self.evidence_viewer.view_changed.connect( self.reference_viewer.change_view) self.reference_viewer.view_changed.connect( self.evidence_viewer.change_view) top_layout = QHBoxLayout() top_layout.addWidget(load_button) top_layout.addWidget(self.file_label) top_layout.addStretch() top_layout.addWidget(self.comp_label) top_layout.addWidget(self.normal_radio) top_layout.addWidget(self.map_radio) top_layout.addWidget(self.diff_radio) top_layout.addWidget(self.gray_check) top_layout.addWidget(self.equal_check) index_layout = QVBoxLayout() index_label = QLabel(self.tr('Image Quality Assessment')) modify_font(index_label, bold=True) index_layout.addWidget(index_label) index_layout.addWidget(self.table_widget) center_layout = QHBoxLayout() center_layout.addWidget(self.evidence_viewer) center_layout.addWidget(self.reference_viewer) center_layout.addLayout(index_layout) main_layout = QVBoxLayout() main_layout.addLayout(top_layout) main_layout.addLayout(center_layout) self.setLayout(main_layout) def load(self): filename, basename, image = load_image(self) if filename is None: return if image.shape != self.evidence.shape: QMessageBox.critical( self, self.tr('Error'), self.tr('Evidence and reference must have the same size!')) return self.file_label.setText(basename) self.reference = image self.difference = normalize_mat( cv.absdiff(self.evidence, self.reference)) self.equalized = cv.merge( [cv.equalizeHist(c) for c in cv.split(self.difference)]) x = cv.cvtColor(self.evidence, cv.COLOR_BGR2GRAY).astype(np.float32) y = cv.cvtColor(self.reference, cv.COLOR_BGR2GRAY).astype(np.float32) mse = self.mse(x, y) covar = self.covar(x, y) psnr = self.psnr(mse) ssim, self.indexmap = self.ssim(x, y) corr = self.corr(x, y) self.table_widget.setItem(0, 1, QTableWidgetItem('{:.4f}'.format(mse))) self.table_widget.setItem(1, 1, QTableWidgetItem('{:.4f}'.format(covar))) if psnr > 0: self.table_widget.setItem( 2, 1, QTableWidgetItem('{:.2f} dB'.format(psnr))) else: self.table_widget.setItem( 2, 1, QTableWidgetItem('+' + u'\u221e' + ' dB')) self.table_widget.setItem(3, 1, QTableWidgetItem('{:.4f}'.format(ssim))) self.table_widget.setItem(4, 1, QTableWidgetItem('{:.4f}'.format(corr))) self.table_widget.setEnabled(True) self.comp_label.setEnabled(True) self.normal_radio.setEnabled(True) self.diff_radio.setEnabled(True) self.gray_check.setEnabled(True) self.equal_check.setEnabled(True) self.map_radio.setEnabled(True) self.change() def change(self): self.gray_check.setEnabled(False) self.equal_check.setEnabled(False) if self.normal_radio.isChecked(): self.reference_viewer.update_original(self.reference) self.last_radio = self.normal_radio elif self.diff_radio.isChecked(): self.gray_check.setEnabled(True) self.equal_check.setEnabled(True) if self.equal_check.isChecked(): result = self.equalized else: result = self.difference if self.gray_check.isChecked(): result = desaturate(result) self.reference_viewer.update_original(result) self.last_radio = self.diff_radio elif self.map_radio.isChecked(): self.reference_viewer.update_original(self.indexmap) self.last_radio = self.map_radio else: self.last_radio.setChecked(True) @staticmethod def ssim(x, y): c1 = 6.5025 c2 = 58.5225 k = (11, 11) s = 1.5 x2 = x**2 y2 = y**2 xy = x * y mu_x = cv.GaussianBlur(x, k, s) mu_y = cv.GaussianBlur(y, k, s) mu_x2 = mu_x**2 mu_y2 = mu_y**2 mu_xy = mu_x * mu_y s_x2 = cv.GaussianBlur(x2, k, s) - mu_x2 s_y2 = cv.GaussianBlur(y2, k, s) - mu_y2 s_xy = cv.GaussianBlur(xy, k, s) - mu_xy t1 = 2 * mu_xy + c1 t2 = 2 * s_xy + c2 t3 = t1 * t2 t1 = mu_x2 + mu_y2 + c1 t2 = s_x2 + s_y2 + c2 t1 *= t2 indexmap = cv.divide(t3, t1) ssim = cv.mean(indexmap)[0] return ssim, 255 - normalize_mat(indexmap, to_bgr=True) @staticmethod def corr(x, y): return np.corrcoef(x, y)[0, 1] @staticmethod def covar(x, y): return np.std(cv.absdiff(x, y)) @staticmethod def mse(x, y): return cv.mean(cv.pow(x - y, 2))[0] @staticmethod def psnr(mse): k = math.sqrt(mse) if k == 0: return -1 return 20 * math.log10((255**2) / k)
class Natang_mmdmaya(QWidget): ''' mmdモデルからmayaへインポートするためのウィンドウ ''' def __init__(self, parent): QWidget.__init__(self) self.setAcceptDrops(True) self.parent = parent self.setWindowFlags(Qt.WindowStaysOnTopHint) self.setWindowTitle('~ MMD > Maya ~') self.setStyleSheet('font-size: 15px; color: #ddf;') vbl = QVBoxLayout() self.setLayout(vbl) self.file_khatangton = os.path.join(os.path.dirname(__file__), 'asset', 'khatangton1.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() yaek_poly = int(f.readline().split('=')[-1].strip()) ao_bs = int(f.readline().split('=')[-1].strip()) ao_kraduk = int(f.readline().split('=')[-1].strip()) watsadu = int(f.readline().split('=')[-1].strip()) pit_mai = int(f.readline().split('=')[-1].strip()) except: chue_tem_file = '' satsuan = '8' yaek_poly = 0 ao_bs = 1 ao_kraduk = 1 watsadu = 4 pit_mai = 1 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_yaek_poly = QCheckBox('材質ごとにポリゴンを分割する') vbl.addWidget(self.cb_yaek_poly) self.cb_ao_kraduk = QCheckBox('骨も作る') vbl.addWidget(self.cb_ao_kraduk) self.cb_ao_bs = QCheckBox('ブレンドシェープも作る') vbl.addWidget(self.cb_ao_bs) hbl = QHBoxLayout() vbl.addLayout(hbl) hbl.addWidget(QLabel('材質')) self.cbb_watsadu = QComboBox() hbl.addWidget(self.cbb_watsadu) self.cbb_watsadu.addItem('無い') self.cbb_watsadu.addItem('blinn') self.cbb_watsadu.addItem('phong') self.cbb_watsadu.addItem('lambert') self.cbb_watsadu.addItem('standardSurface') hbl.addStretch() 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_yaek_poly.setChecked(yaek_poly) self.cb_ao_kraduk.setChecked(ao_kraduk) self.cb_ao_bs.setChecked(ao_bs) self.cbb_watsadu.setCurrentIndex(watsadu) self.cb_yaek_poly.toggled.connect( lambda: self.chue_thuk_kae(self.le_chue_file.text())) self.cb_pit.setChecked(pit_mai) def chue_thuk_kae(self, chue_file): # ファイルの名前が更新されたら sakun = chue_file.split('.')[-1] sang_dai = (sakun.lower() in ['pmd', 'pmx', 'x']) self.btn_roem_sang.setEnabled(sang_dai) self.btn_roem_sang.setStyleSheet( ['text-decoration: line-through; color: #aab;', ''][sang_dai]) dai = chue_file[-2:] != '.x' and not self.cb_yaek_poly.isChecked() self.cb_ao_kraduk.setEnabled(dai) self.cb_ao_bs.setEnabled(dai) self.cb_ao_kraduk.setStyleSheet( ['text-decoration: line-through; color: #aab;', ''][dai]) self.cb_ao_bs.setStyleSheet( ['text-decoration: line-through; color: #aab;', ''][dai]) def khon_file(self): # 読み込むファイルをブラウスする self.setWindowFlags(self.windowFlags() & ~Qt.WindowStaysOnTopHint) chue_file, ok = QFileDialog.getOpenFileName( filter='PMD/PMX/X (*.pmd *.pmx *.x)') if (ok): self.le_chue_file.setText(chue_file) self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint) self.show() def dragEnterEvent(self, e): if (e.mimeData().hasUrls()): # ファイルがドラッグされたら使える e.accept() def dropEvent(self, e): self.le_chue_file.setText( e.mimeData().urls()[0].toLocalFile()) # ドラッグされたファイルの名前を取得する 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. yaek_poly = self.cb_yaek_poly.isChecked() ao_bs = not yaek_poly and self.cb_ao_bs.isChecked() ao_kraduk = not yaek_poly and self.cb_ao_kraduk.isChecked() watsadu = self.cbb_watsadu.currentIndex() # ここでmayaのシーンの中でモデルを作る pmxpaimaya.sang(chue_tem_file, satsuan, yaek_poly, ao_bs, ao_kraduk, watsadu) 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' % yaek_poly) f.write('ブレンドシェープ = %d\n' % ao_bs) f.write('ジョイント = %d\n' % ao_kraduk) f.write('材質 = %d\n' % watsadu) f.write('閉じる = %d\n' % pit_mai) if (pit_mai): # 終わったらこのウィンドウを閉じる self.close() def keyPressEvent(self, e): # escが押されたら閉じる if (e.key() == Qt.Key_Escape): self.close() def closeEvent(self, e): self.parent.natangyoi['mmdmaya'] = None
class PreferencesGeneralPage(QWidget): preferencesChanged = Signal() def __init__(self, parent=None): super().__init__(parent) # Title title = QLabel(self.tr("<strong style=\"font-size:large;\">{0}</strong>").format(self.title())) # # Content: Geometry & State self._chkRestoreApplicationGeometry = QCheckBox(self.tr("Save and restore the application geometry")) self._chkRestoreApplicationGeometry.stateChanged.connect(self._onPreferencesChanged) self._chkRestoreApplicationState = QCheckBox(self.tr("Save and restore the application state")) self._chkRestoreApplicationState.stateChanged.connect(self._onPreferencesChanged) geometryStateLayout = QVBoxLayout() geometryStateLayout.addWidget(self._chkRestoreApplicationGeometry) geometryStateLayout.addWidget(self._chkRestoreApplicationState) geometryStateGroup = QGroupBox(self.tr("Geometry && State")) geometryStateGroup.setLayout(geometryStateLayout) # # Content: Recently Opened Documents self._spbMaximumRecentDocuments = QSpinBox() self._spbMaximumRecentDocuments.setRange(0, 25) self._spbMaximumRecentDocuments.setToolTip(self.tr("Maximum number of recently opened documents")) self._spbMaximumRecentDocuments.valueChanged.connect(self._onPreferencesChanged) self._spbMaximumRecentDocuments.valueChanged[int].connect(self._onMaximumRecentDocumentsChanged) self._chkRestoreRecentDocuments = QCheckBox(self.tr("Save and restore documents")) self._chkRestoreRecentDocuments.stateChanged.connect(self._onPreferencesChanged) recentDocumentsFormLayout = QFormLayout() recentDocumentsFormLayout.addRow(self.tr("Number of documents"), self._spbMaximumRecentDocuments) recentDocumentsLayout = QVBoxLayout() recentDocumentsLayout.addLayout(recentDocumentsFormLayout) recentDocumentsLayout.addWidget(self._chkRestoreRecentDocuments) recentDocumentsGroup = QGroupBox(self.tr("Recently Opened Documents")) recentDocumentsGroup.setLayout(recentDocumentsLayout) # Main layout self._layout = QVBoxLayout(self) self._layout.addWidget(title) self._layout.addWidget(geometryStateGroup) self._layout.addWidget(recentDocumentsGroup) self._layout.addStretch(1) def setZeroMargins(self): self._layout.setContentsMargins(0, 0, 0, 0) def title(self): return self.tr("General") def _onPreferencesChanged(self): self.preferencesChanged.emit() def _onMaximumRecentDocumentsChanged(self, val): self._chkRestoreRecentDocuments.setEnabled(val > 0) def setRestoreApplicationGeometry(self, checked): self._chkRestoreApplicationGeometry.setChecked(checked) def restoreApplicationGeometry(self): return self._chkRestoreApplicationGeometry.isChecked() def setRestoreApplicationState(self, checked): self._chkRestoreApplicationState.setChecked(checked) def restoreApplicationState(self): return self._chkRestoreApplicationState.isChecked() def setMaximumRecentDocuments(self, val): self._spbMaximumRecentDocuments.setValue(val) def maximumRecentDocuments(self): return self._spbMaximumRecentDocuments.value() def setRestoreRecentDocuments(self, checked): self._chkRestoreRecentDocuments.setChecked(checked) def restoreRecentDocuments(self): return self._chkRestoreRecentDocuments.isChecked()
class TrainingPanel(Panel): def __init__(self, datasets, testing_panel, threads): super().__init__() if isinstance(testing_panel, TestingPanel): self.testing_panel = testing_panel else: raise TypeError('"testing_panel" must be the instance of ' '"TestingPanel"') self.datasets = datasets self.threads = threads self.__set_execution_ui() self.__set_options_ui() self.__set_outputs_ui() self.__set_graphic_ui() def __set_execution_ui(self): group_box = QGroupBox('Training Execution') inner_layout = QHBoxLayout() group_box.setLayout(inner_layout) self.data_selector = QComboBox() self.data_selector.addItems(list(self.datasets.keys())) self.data_selector.setStatusTip('Select the training dataset.') self.start_btn = QPushButton('Train') self.start_btn.setStatusTip('Start training.') self.start_btn.clicked.connect(self.__run) self.stop_btn = QPushButton('Stop') self.stop_btn.setStatusTip('Force the training stop running.') self.stop_btn.setDisabled(True) self.multicore_cb = QCheckBox('Multicore') self.multicore_cb.setStatusTip('Use multiprocessing in calculating ' 'fitting for populations.') self.multicore_cb.setChecked(True) inner_layout.addWidget(self.data_selector, 1) inner_layout.addWidget(self.start_btn) inner_layout.addWidget(self.stop_btn) inner_layout.addWidget(self.multicore_cb) self._layout.addWidget(group_box) def __set_options_ui(self): group_box = QGroupBox('Training Options') inner_layout = QFormLayout() group_box.setLayout(inner_layout) self.iter_times = QSpinBox() self.iter_times.setRange(1, 1000000) self.iter_times.setValue(200) self.iter_times.setStatusTip('The total iterating times for training.') self.population_size = QSpinBox() self.population_size.setRange(1, 100000) self.population_size.setValue(100) self.population_size.setStatusTip('The population size for the PSO.') self.inertia_weight = QDoubleSpinBox() self.inertia_weight.setRange(0, 50) self.inertia_weight.setValue(1) self.inertia_weight.setSingleStep(0.1) self.inertia_weight.setStatusTip('The inertia weight of the velocity ' ' for each individual.') self.cognitive_const_rand_upper = QDoubleSpinBox() self.cognitive_const_rand_upper.setRange(0, 50) self.cognitive_const_rand_upper.setValue(2) self.cognitive_const_rand_upper.setSingleStep(0.1) self.cognitive_const_rand_upper.setStatusTip( 'The random upper bound for cognitive accelerate constant.') self.social_const_rand_upper = QDoubleSpinBox() self.social_const_rand_upper.setRange(0, 50) self.social_const_rand_upper.setValue(3) self.social_const_rand_upper.setSingleStep(0.1) self.social_const_rand_upper.setStatusTip( 'The random upper bound for social accelerate constant.') self.v_max = QDoubleSpinBox() self.v_max.setRange(0.5, 100) self.v_max.setValue(5) self.v_max.setSingleStep(1) self.v_max.setStatusTip('The maximum of velocity for each individual.') self.nneuron = QSpinBox() self.nneuron.setRange(1, 100) self.nneuron.setValue(6) self.nneuron.setStatusTip('The number of RBFN neuron.') self.sd_max = QDoubleSpinBox() self.sd_max.setRange(0.01, 20) self.sd_max.setValue(10) self.sd_max.setSingleStep(0.1) self.sd_max.setStatusTip('The random range maximum of standard ' 'deviation of each neuron in RBFN (only for ' 'initialization).') inner_layout.addRow('Iterating Times:', self.iter_times) inner_layout.addRow('Population Size:', self.population_size) inner_layout.addRow('Inertia Weight:', self.inertia_weight) inner_layout.addRow('Cognitive Const Upper:', self.cognitive_const_rand_upper) inner_layout.addRow('Social Const Upper:', self.social_const_rand_upper) inner_layout.addRow('Maximum of Velocity:', self.v_max) inner_layout.addRow('Number of Neuron:', self.nneuron) inner_layout.addRow('Maximum of SD:', self.sd_max) self._layout.addWidget(group_box) def __set_outputs_ui(self): group_box = QGroupBox('Training Details') inner_layout = QFormLayout() group_box.setLayout(inner_layout) self.current_iter_time = QLabel('--') self.current_error = QLabel('--') self.avg_error = QLabel('--') self.global_best_error = QLabel('--') self.total_best_error = QLabel('--') self.progressbar = QProgressBar() self.current_iter_time.setAlignment(Qt.AlignCenter) self.current_error.setAlignment(Qt.AlignCenter) self.avg_error.setAlignment(Qt.AlignCenter) self.global_best_error.setAlignment(Qt.AlignCenter) self.total_best_error.setAlignment(Qt.AlignCenter) self.current_iter_time.setStatusTip('The current iterating time of ' 'the PSO.') self.current_error.setStatusTip('The current error from the fitting ' 'function. ("( )": normalized error)') self.avg_error.setStatusTip('The average error from the fitting ' 'function in current iteration. ("( )": ' 'normalized error)') self.global_best_error.setStatusTip( 'The error of global best individual from the fitting function in ' 'current iteration. ("( )": normalized error)') self.total_best_error.setStatusTip( 'The error of total best individual from the fitting function in ' 'training. ("( )": normalized error)') inner_layout.addRow('Current Iterating Time:', self.current_iter_time) inner_layout.addRow('Current Error:', self.current_error) inner_layout.addRow('Average Error:', self.avg_error) inner_layout.addRow('Global Best Error:', self.global_best_error) inner_layout.addRow('Total Best Error:', self.total_best_error) inner_layout.addRow(self.progressbar) self._layout.addWidget(group_box) def __set_graphic_ui(self): group_box = QGroupBox('Error Line Charts:') inner_layout = QVBoxLayout() group_box.setLayout(inner_layout) self.err_chart = ErrorLineChart(1) self.err_chart.setStatusTip('The history of error from the fitting ' 'of the PSO for each data.') self.__err_x = 1 self.iter_err_chart = ErrorLineChart( 3, ('Avg', 'Global Best', 'Total Best')) self.iter_err_chart.setStatusTip('The history of average and least ' 'error from the fitting of the PSO ' 'for each iteration.') self.iter_err_chart.setMinimumHeight(150) inner_layout.addWidget(QLabel('Current Error')) inner_layout.addWidget(self.err_chart) inner_layout.addWidget(QLabel('Average Error')) inner_layout.addWidget(self.iter_err_chart) self._layout.addWidget(group_box) @Slot() def __init_widgets(self): self.start_btn.setDisabled(True) self.stop_btn.setEnabled(True) self.multicore_cb.setDisabled(True) self.data_selector.setDisabled(True) self.iter_times.setDisabled(True) self.population_size.setDisabled(True) self.inertia_weight.setDisabled(True) self.cognitive_const_rand_upper.setDisabled(True) self.social_const_rand_upper.setDisabled(True) self.v_max.setDisabled(True) self.nneuron.setDisabled(True) self.sd_max.setDisabled(True) self.err_chart.clear() self.iter_err_chart.clear() self.__err_x = 1 @Slot() def __reset_widgets(self): self.start_btn.setEnabled(True) self.stop_btn.setDisabled(True) self.multicore_cb.setEnabled(True) self.data_selector.setEnabled(True) self.iter_times.setEnabled(True) self.population_size.setEnabled(True) self.inertia_weight.setEnabled(True) self.cognitive_const_rand_upper.setEnabled(True) self.social_const_rand_upper.setEnabled(True) self.v_max.setEnabled(True) self.nneuron.setEnabled(True) self.sd_max.setEnabled(True) self.progressbar.setMinimum(0) self.progressbar.setMaximum(100) @Slot() def __indicate_busy(self): self.progressbar.setMinimum(0) self.progressbar.setMaximum(0) @Slot(int) def __show_current_iter_time(self, value): self.current_iter_time.setText(str(value + 1)) self.progressbar.setValue(value + 1) @Slot(float) def __show_current_error(self, value): self.current_error.setText('{:.5f} ({:.5f})'.format(value, value / 40)) self.err_chart.append_point(self.__err_x, value) self.__err_x += 1 @Slot(float, float, float) def __show_iter_error(self, avg, glob, total): self.avg_error.setText('{:.5f} ({:.5f})'.format(avg, avg / 40)) self.global_best_error.setText( '{:.5f} ({:.5f})'.format(glob, glob / 40)) self.total_best_error.setText( '{:.5f} ({:.5f})'.format(total, total / 40)) self.iter_err_chart.append_point( int(self.current_iter_time.text()), total, 2) self.iter_err_chart.append_point( int(self.current_iter_time.text()), glob, 1) self.iter_err_chart.append_point( int(self.current_iter_time.text()), avg, 0) def __run(self): self.progressbar.setMaximum(self.iter_times.value()) self.__current_dataset = self.datasets[ self.data_selector.currentText()] self.__pso = PSO(self.iter_times.value(), self.population_size.value(), self.inertia_weight.value(), self.cognitive_const_rand_upper.value(), self.social_const_rand_upper.value(), self.v_max.value(), self.nneuron.value(), self.__current_dataset, self.sd_max.value(), is_multicore=self.multicore_cb.isChecked()) self.threads.append(self.__pso) self.stop_btn.clicked.connect(self.__pso.stop) self.__pso.started.connect(self.__init_widgets) self.__pso.finished.connect(self.__reset_widgets) self.__pso.sig_current_iter_time.connect(self.__show_current_iter_time) self.__pso.sig_current_error.connect(self.__show_current_error) self.__pso.sig_iter_error.connect(self.__show_iter_error) self.__pso.sig_indicate_busy.connect(self.__indicate_busy) self.__pso.sig_console.connect(self.testing_panel.print_console) self.__pso.sig_rbfn.connect(self.testing_panel.load_rbfn) self.__pso.start()
class MainWindow(QMainWindow): def __init__(self, app, parent=None): super(MainWindow, self).__init__(parent) self.imagesDir = app.dir + '/images/' self.setWindowIcon(QIcon(self.imagesDir + 'icon.png')) self.path = '' self.settings = QSettings() self.lastDir = self.settings.value('lastDir', '') self.setMinimumWidth(540) self.supportedFormats = [] for f in QImageReader.supportedImageFormats(): self.supportedFormats.append(str(f.data(), encoding="utf-8")) self.fileWatcher = QFileSystemWatcher() self.fileWatcher.fileChanged.connect(self.fileChanged) # widgets self.showPixmapWidget = None self.tileWidthSpinBox = QSpinBox() self.tileWidthSpinBox.setValue(16) self.tileWidthSpinBox.setFixedWidth(50) self.tileWidthSpinBox.setMinimum(1) self.tileHeightSpinBox = QSpinBox() self.tileHeightSpinBox.setValue(16) self.tileHeightSpinBox.setFixedWidth(50) self.tileHeightSpinBox.setMinimum(1) self.paddingSpinBox = QSpinBox() self.paddingSpinBox.setFixedWidth(50) self.paddingSpinBox.setMinimum(1) self.transparentCheckbox = QCheckBox("Transparent") self.transparentCheckbox.setChecked(True) self.transparentCheckbox.stateChanged.connect(self.transparentChanged) self.backgroundColorEdit = ColorEdit() self.backgroundColorEdit.setEnabled(False) self.backgroundColorLabel = QLabel("Background color:") self.backgroundColorLabel.setEnabled(False) self.forcePotCheckBox = QCheckBox("Force PoT") self.forcePotCheckBox.setChecked(True) self.forcePotCheckBox.stateChanged.connect(self.forcePotChanged) self.reorderTilesCheckBox = QCheckBox("Reorder tiles") self.generateAndExportButton = QPushButton("Generate and export") self.generateAndExportButton.setFixedHeight(32) self.generateAndExportButton.clicked.connect(self.generateAndExportClicked) self.generateAndExportButton.setEnabled(False) self.pixmapWidget = PixmapWidget() self.pixmapWidget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.pixmapWidget.setPixmap(self.createDropTextPixmap()) self.pixmapWidget.dropSignal.connect(self.fileDropped) self.pixmapWidget.setMinimumHeight(300) # load settings self.tileWidthSpinBox.setValue(int(self.settings.value('tileWidth', 16))) self.tileHeightSpinBox.setValue(int(self.settings.value('tileHeight', 16))) self.paddingSpinBox.setValue(int(self.settings.value('padding', 1))) self.forcePotCheckBox.setChecked(True if self.settings.value('forcePot', 'true') == 'true' else False) self.reorderTilesCheckBox.setChecked(True if self.settings.value('reorderTiles', 'false') == 'true' else False) self.transparentCheckbox.setChecked(True if self.settings.value('transparent', 'false') == 'true' else False) self.backgroundColorEdit.setColorText(str(self.settings.value('backgroundColor', '#FF00FF'))) self.restoreGeometry(QByteArray(self.settings.value('MainWindow/geometry'))) self.restoreState(QByteArray(self.settings.value('MainWindow/windowState'))) # layout hl1 = QHBoxLayout() hl1.setContentsMargins(5, 5, 5, 5) hl1.addWidget(QLabel("Tile width:")) hl1.addSpacing(5) hl1.addWidget(self.tileWidthSpinBox) hl1.addSpacing(15) hl1.addWidget(QLabel("Tile height:")) hl1.addSpacing(5) hl1.addWidget(self.tileHeightSpinBox) hl1.addSpacing(15) hl1.addWidget(QLabel("Padding:")) hl1.addSpacing(5) hl1.addWidget(self.paddingSpinBox) hl1.addSpacing(15) hl1.addWidget(self.forcePotCheckBox) hl1.addSpacing(15) hl1.addWidget(self.reorderTilesCheckBox) hl1.addStretch() hl2 = QHBoxLayout() hl2.setContentsMargins(5, 5, 5, 5) hl2.addWidget(self.transparentCheckbox) hl2.addSpacing(15) hl2.addWidget(self.backgroundColorLabel) hl2.addSpacing(5) hl2.addWidget(self.backgroundColorEdit) hl2.addStretch() hl3 = QHBoxLayout() hl3.setContentsMargins(5, 5, 5, 5) hl3.addWidget(self.generateAndExportButton) vl = QVBoxLayout() vl.setContentsMargins(0, 0, 0, 0) vl.setSpacing(0) vl.addLayout(hl1) vl.addLayout(hl2) vl.addWidget(self.pixmapWidget) vl.addLayout(hl3) w = QWidget() w.setLayout(vl) self.setCentralWidget(w) self.setTitle() def setTitle(self): p = ' - ' + os.path.basename(self.path) if self.path else '' self.setWindowTitle(QCoreApplication.applicationName() + ' ' + QCoreApplication.applicationVersion() + p) def createDropTextPixmap(self): pixmap = QPixmap(481, 300) pixmap.fill(QColor("#333333")) painter = QPainter(pixmap) font = QFont("Arial") font.setPixelSize(28) font.setBold(True) fm = QFontMetrics(font) painter.setFont(font) painter.setPen(QPen(QColor("#888888"), 1)) text = "Drop the tileset image here" x = (pixmap.width()-fm.width(text))/2 y = (pixmap.height()+fm.height())/2 painter.drawText(x, y, text) del painter return pixmap def fileDropped(self, path): path = str(path) name, ext = os.path.splitext(path) ext = ext[1:] if not ext in self.supportedFormats: QMessageBox.warning(self, "Warning", "The dropped file is not supported") return pixmap = QPixmap(path) if pixmap.isNull(): QMessageBox.warning(self, "Warning", "Can't load the image") return if self.path: self.fileWatcher.removePath(self.path) self.path = path self.fileWatcher.addPath(self.path) self.pixmapWidget.setPixmap(pixmap) self.generateAndExportButton.setEnabled(True) self.setTitle() self.activateWindow() def fileChanged(self, path): #self.fileDropped(path) pass def transparentChanged(self): e = self.transparentCheckbox.isChecked() self.backgroundColorEdit.setEnabled(not e) self.backgroundColorLabel.setEnabled(not e) def forcePotChanged(self): e = self.forcePotCheckBox.isChecked() self.reorderTilesCheckBox.setEnabled(e) def generateAndExportClicked(self): g = Generator() g.tileWidth = self.tileWidthSpinBox.value() g.tileHeight = self.tileHeightSpinBox.value() g.forcePot = self.forcePotCheckBox.isChecked() g.isTransparent = self.transparentCheckbox.isChecked() g.bgColor = self.backgroundColorEdit.getColor() g.reorder = self.reorderTilesCheckBox.isChecked() g.padding = self.paddingSpinBox.value() target = g.create(self.pixmapWidget.pixmap); # export self.lastDir = os.path.dirname(self.path) targetPath = QFileDialog.getSaveFileName(self, 'Export', self.lastDir, 'PNG (*.png)') if targetPath: target.save(targetPath[0]) showPixmap = QPixmap.fromImage(target) if self.showPixmapWidget: self.showPixmapWidget.deleteLater() del self.showPixmapWidget self.showPixmapWidget = PixmapWidget() self.showPixmapWidget.setWindowIcon(self.windowIcon()) self.showPixmapWidget.setWindowTitle(os.path.basename(targetPath[0])) self.showPixmapWidget.resize(showPixmap.width(), showPixmap.height()) self.showPixmapWidget.setPixmap(showPixmap) self.showPixmapWidget.show() def closeEvent(self, event): if self.showPixmapWidget: self.showPixmapWidget.close() # save settings self.settings.setValue('tileWidth', self.tileWidthSpinBox.value()) self.settings.setValue('tileHeight', self.tileHeightSpinBox.value()) self.settings.setValue('padding', self.paddingSpinBox.value()) self.settings.setValue('forcePot', self.forcePotCheckBox.isChecked()) self.settings.setValue('reorderTiles', self.reorderTilesCheckBox.isChecked()) self.settings.setValue('transparent', self.transparentCheckbox.isChecked()) self.settings.setValue('backgroundColor', self.backgroundColorEdit.getColor().name()) self.settings.setValue('lastDir', self.lastDir) self.settings.setValue('MainWindow/geometry', self.saveGeometry()) self.settings.setValue('MainWindow/windowState', self.saveState()) super(MainWindow, self).closeEvent(event)
class NGL_HKLViewer(QWidget): def __init__(self, parent=None): super(NGL_HKLViewer, self).__init__(parent) self.verbose = 0 self.UseOSbrowser = False self.jscriptfname = "" self.devmode = False for e in sys.argv: if "verbose" in e: self.verbose = e.split("verbose=")[1] if "UseOSbrowser" in e: self.UseOSbrowser = e.split("UseOSbrowser=")[1] if "jscriptfname" in e: self.jscriptfname = e.split("jscriptfname=")[1] if "devmode" in e: self.devmode = True self.zmq_context = None self.bufsize = 20000 self.originalPalette = QApplication.palette() self.openFileNameButton = QPushButton("Load reflection file") self.openFileNameButton.setDefault(True) self.openFileNameButton.clicked.connect(self.OpenReflectionsFile) self.debugbutton = QPushButton("Debug") self.debugbutton.clicked.connect(self.DebugInteractively) self.settingsbtn = QPushButton("Settings") self.settingsbtn.clicked.connect(self.SettingsDialog) self.mousemoveslider = QSlider(Qt.Horizontal) self.mousemoveslider.setMinimum(0) self.mousemoveslider.setMaximum(300) self.mousemoveslider.setValue(0) self.mousemoveslider.sliderReleased.connect( self.onFinalMouseSensitivity) self.mousemoveslider.valueChanged.connect(self.onMouseSensitivity) self.mousesensitxtbox = QLineEdit('') self.mousesensitxtbox.setReadOnly(True) self.fontspinBox = QDoubleSpinBox() self.fontspinBox.setSingleStep(1) self.fontspinBox.setRange(4, 50) self.font = QFont() self.font.setFamily(self.font.defaultFamily()) self.fontspinBox.setValue(self.font.pointSize()) #self.fontspinBox.setValue(self.font.pixelSize()) self.fontspinBox.valueChanged.connect(self.onFontsizeChanged) self.Fontsize_labeltxt = QLabel() self.Fontsize_labeltxt.setText("Font size:") self.cameraPerspectCheckBox = QCheckBox() self.cameraPerspectCheckBox.setText("Perspective camera") self.cameraPerspectCheckBox.clicked.connect(self.onCameraPerspect) self.cameraPerspectCheckBox.setCheckState(Qt.Unchecked) self.settingsform = SettingsForm(self) self.MillerComboBox = QComboBox() self.MillerComboBox.activated.connect(self.onMillerComboSelchange) #self.MillerComboBox.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.MillerLabel = QLabel() self.MillerLabel.setText("Selected HKL Scene") self.HKLnameedit = QLineEdit('') self.HKLnameedit.setReadOnly(True) self.textInfo = QTextEdit() self.textInfo.setLineWrapMode(QTextEdit.NoWrap) self.textInfo.setReadOnly(True) labels = [ "Label", "Type", "no. of HKLs", "Span of HKLs", "Min Max data", "Min Max sigmas", "d_min, d_max", "Symmetry unique", "Anomalous" ] self.millertable = QTableWidget(0, len(labels)) self.millertable.setHorizontalHeaderLabels(labels) self.millertable.horizontalHeader().setDefaultAlignment(Qt.AlignLeft) # don't allow editing this table self.millertable.setEditTriggers(QTableWidget.NoEditTriggers) self.createExpansionBox() self.createFileInfoBox() self.CreateSliceTabs() self.createRadiiScaleGroupBox() self.createBinsBox() self.CreateFunctionTabs() mainLayout = QGridLayout() mainLayout.addWidget(self.FileInfoBox, 0, 0) mainLayout.addWidget(self.MillerLabel, 1, 0) mainLayout.addWidget(self.MillerComboBox, 2, 0) mainLayout.addWidget(self.functionTabWidget, 3, 0) mainLayout.addWidget(self.settingsbtn, 4, 0, 1, 1) #import code, traceback; code.interact(local=locals(), banner="".join( traceback.format_stack(limit=10) ) ) if self.UseOSbrowser == False: self.BrowserBox = QWebEngineView() mainLayout.addWidget(self.BrowserBox, 0, 1, 5, 3) self.BrowserBox.setUrl("https://cctbx.github.io/") #self.BrowserBox.setUrl("https://webglreport.com/") #self.BrowserBox.loadFinished.connect(self.onLoadFinished) mainLayout.setColumnStretch(2, 1) mainLayout.setRowStretch(0, 1) mainLayout.setRowStretch(1, 0) mainLayout.setRowStretch(2, 1) mainLayout.setRowStretch(3, 1) mainLayout.setColumnStretch(4, 0) self.setLayout(mainLayout) self.setWindowTitle("HKL-Viewer") self.cctbxproc = None self.LaunchCCTBXPython() self.out = None self.err = None self.comboviewwidth = 0 self.hklscenes_arrays = [] self.array_infotpls = [] self.matching_arrays = [] self.bin_infotpls = None self.bin_opacities = None self.html_url = "" self.spacegroups = [] self.info = [] self.infostr = "" self.fileisvalid = False self.NewFileLoaded = False self.NewHKLscenes = False self.updatingNbins = False self.binstableitemchanges = False self.show() def SettingsDialog(self): self.settingsform.show() def update(self): if self.cctbxproc: if self.cctbxproc.stdout: print(self.cctbxproc.stdout.read().decode("utf-8")) if self.cctbxproc.stderr: print(self.cctbxproc.stderr.read().decode("utf-8")) if self.out: print(self.out.decode("utf-8")) if self.err: print(self.err.decode("utf-8")) if self.zmq_context: try: msg = self.socket.recv( flags=zmq.NOBLOCK ) #To empty the socket from previous messages msgstr = msg.decode() self.infodict = eval(msgstr) #print("received from cctbx: " + str(self.infodict)) if self.infodict: if self.infodict.get("hklscenes_arrays"): self.hklscenes_arrays = self.infodict.get( "hklscenes_arrays", []) if self.infodict.get("array_infotpls"): self.array_infotpls = self.infodict.get( "array_infotpls", []) if self.infodict.get("bin_data_label"): self.BinDataComboBox.setCurrentText( self.infodict["bin_data_label"]) if self.infodict.get("bin_infotpls"): self.bin_infotpls = self.infodict["bin_infotpls"] self.nbins = len(self.bin_infotpls) self.updatingNbins = True self.Nbins_spinBox.setValue(self.nbins) self.updatingNbins = False self.binstable.clearContents() self.binstable.setRowCount(self.nbins) for row, bin_infotpl in enumerate(self.bin_infotpls): for col, elm in enumerate(bin_infotpl): # only allow changing the last column with opacity values if col != 3: item = QTableWidgetItem(str(elm)) else: item = QTableWidgetItem() item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) item.setCheckState(Qt.Checked) item.setFlags(item.flags() ^ Qt.ItemIsEditable) self.binstable.setItem(row, col, item) if self.bin_opacities: self.update_table_opacities() if self.infodict.get("bin_opacities"): self.bin_opacities = self.infodict["bin_opacities"] if self.binstable.rowCount() > 0: self.update_table_opacities() if self.infodict.get("html_url"): self.html_url = self.infodict["html_url"] if self.UseOSbrowser == False: self.BrowserBox.setUrl(self.html_url) # workaround for background colour bug in chromium # https://bugreports.qt.io/browse/QTBUG-41960 self.BrowserBox.page().setBackgroundColor( QColor(100, 100, 100, 1.0)) if self.infodict.get("spacegroups"): self.spacegroups = self.infodict.get("spacegroups", []) self.SpaceGroupComboBox.clear() self.SpaceGroupComboBox.addItems(self.spacegroups) if self.infodict.get("merge_data"): self.mergedata = self.infodict["merge_data"] currentinfostr = "" if self.infodict.get("info"): currentinfostr = self.infodict.get("info", []) if self.infodict.get("NewFileLoaded"): self.NewFileLoaded = self.infodict.get( "NewFileLoaded", False) if self.infodict.get("NewHKLscenes"): self.NewHKLscenes = self.infodict.get( "NewHKLscenes", False) self.fileisvalid = True #print("ngl_hkl_infodict: " + str(ngl_hkl_infodict)) if currentinfostr: #print(currentinfostr) self.infostr += currentinfostr + "\n" # display no more than self.bufsize bytes of text self.infostr = self.infostr[-self.bufsize:] self.textInfo.setPlainText(self.infostr) self.textInfo.verticalScrollBar().setValue( self.textInfo.verticalScrollBar().maximum()) if self.NewFileLoaded and self.NewHKLscenes: #if self.mergedata == True : val = Qt.CheckState.Checked #if self.mergedata == None : val = Qt.CheckState.PartiallyChecked #if self.mergedata == False : val = Qt.CheckState.Unchecked #self.mergecheckbox.setCheckState(val ) #print("got hklscenes: " + str(self.hklscenes_arrays)) self.MillerComboBox.clear() self.MillerComboBox.addItems( [e[3] for e in self.hklscenes_arrays]) self.MillerComboBox.setCurrentIndex( -1) # unselect the first item in the list self.comboviewwidth = 0 for e in self.hklscenes_arrays: self.comboviewwidth = max( self.comboviewwidth, self.MillerComboBox.fontMetrics().width(e[3])) self.MillerComboBox.view().setMinimumWidth( self.comboviewwidth) self.millertable.clearContents() self.millertable.setRowCount(len( self.hklscenes_arrays)) for n, millarr in enumerate(self.array_infotpls): for m, elm in enumerate(millarr): self.millertable.setItem( n, m, QTableWidgetItem(str(elm))) self.functionTabWidget.setDisabled(True) self.NewFileLoaded = False if self.NewHKLscenes: self.BinDataComboBox.clear() self.BinDataComboBox.addItems( ["Resolution"] + [e[3] for e in self.hklscenes_arrays]) self.BinDataComboBox.view().setMinimumWidth( self.comboviewwidth) #self.BinDataComboBox.setCurrentIndex(-1) # unselect the first item in the list self.NewHKLscenes = False except Exception as e: errmsg = str(e) if "Resource temporarily unavailable" not in errmsg: print(errmsg + traceback.format_exc(limit=10)) pass def onFinalMouseSensitivity(self): val = self.mousemoveslider.value() / 100.0 self.NGL_HKL_command( 'NGL_HKLviewer.viewer.NGL.mouse_sensitivity = %f' % val) def onMouseSensitivity(self): val = self.mousemoveslider.value() / 100.0 self.mousesensitxtbox.setText("%2.2f" % val) def onFontsizeChanged(self, val): font = app.font() font.setPointSize(val) app.setFont(font) self.settingsform.setFixedSize(self.settingsform.sizeHint()) def onCameraPerspect(self, val): if self.cameraPerspectCheckBox.isChecked(): self.NGL_HKL_command("NGL_HKLviewer.camera_type = perspective") else: self.NGL_HKL_command("NGL_HKLviewer.camera_type = orthographic") def MergeData(self): if self.mergecheckbox.checkState() == Qt.CheckState.Checked: self.NGL_HKL_command('NGL_HKLviewer.mergedata = True') if self.mergecheckbox.checkState() == Qt.CheckState.PartiallyChecked: self.NGL_HKL_command('NGL_HKLviewer.mergedata = None') if self.mergecheckbox.checkState() == Qt.CheckState.Unchecked: self.NGL_HKL_command('NGL_HKLviewer.mergedata = False') def ExpandToP1(self): if self.expandP1checkbox.isChecked(): self.NGL_HKL_command('NGL_HKLviewer.viewer.expand_to_p1 = True') else: self.NGL_HKL_command('NGL_HKLviewer.viewer.expand_to_p1 = False') def ExpandAnomalous(self): if self.expandAnomalouscheckbox.isChecked(): self.NGL_HKL_command( 'NGL_HKLviewer.viewer.expand_anomalous = True') else: self.NGL_HKL_command( 'NGL_HKLviewer.viewer.expand_anomalous = False') def showSysAbsent(self): if self.sysabsentcheckbox.isChecked(): self.NGL_HKL_command( 'NGL_HKLviewer.viewer.show_systematic_absences = True') else: self.NGL_HKL_command( 'NGL_HKLviewer.viewer.show_systematic_absences = False') def showMissing(self): if self.missingcheckbox.isChecked(): self.NGL_HKL_command('NGL_HKLviewer.viewer.show_missing = True') else: self.NGL_HKL_command('NGL_HKLviewer.viewer.show_missing = False') def showOnlyMissing(self): if self.onlymissingcheckbox.isChecked(): self.NGL_HKL_command( 'NGL_HKLviewer.viewer.show_only_missing = True') else: self.NGL_HKL_command( 'NGL_HKLviewer.viewer.show_only_missing = False') def showSlice(self): if self.showslicecheckbox.isChecked(): self.NGL_HKL_command('NGL_HKLviewer.viewer.slice_mode = True') if self.expandP1checkbox.isChecked(): self.NGL_HKL_command("""NGL_HKLviewer.viewer { expand_to_p1 = True inbrowser = False } """) if self.expandAnomalouscheckbox.isChecked(): self.NGL_HKL_command("""NGL_HKLviewer.viewer { expand_anomalous = True inbrowser = False } """) else: self.NGL_HKL_command("""NGL_HKLviewer.viewer { slice_mode = False inbrowser = True } """) def onSliceComboSelchange(self, i): rmin = self.array_infotpls[self.MillerComboBox.currentIndex()][3][0][i] rmax = self.array_infotpls[self.MillerComboBox.currentIndex()][3][1][i] self.sliceindexspinBox.setRange(rmin, rmax) self.NGL_HKL_command("NGL_HKLviewer.viewer.slice_axis = %s" % self.sliceaxis[i]) def onSliceIndexChanged(self, val): self.sliceindex = val self.NGL_HKL_command("NGL_HKLviewer.viewer.slice_index = %d" % self.sliceindex) def onBindataComboSelchange(self, i): if self.BinDataComboBox.currentText(): if self.BinDataComboBox.currentIndex() > 0: bin_scene_label = str(self.BinDataComboBox.currentIndex() - 1) else: bin_scene_label = "Resolution" self.NGL_HKL_command("NGL_HKLviewer.bin_scene_label = %s" % bin_scene_label) def update_table_opacities(self, allalpha=None): bin_opacitieslst = eval(self.bin_opacities) self.binstable_isready = False for binopacity in bin_opacitieslst: if not allalpha: alpha = float(binopacity.split(",")[0]) else: alpha = allalpha bin = int(binopacity.split(",")[1]) item = QTableWidgetItem() item.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) if alpha < 0.5: item.setCheckState(Qt.Unchecked) else: item.setCheckState(Qt.Checked) item.setFlags(item.flags() ^ Qt.ItemIsEditable) self.binstable.setItem(bin, 3, item) self.binstable_isready = True def SetOpaqueAll(self): if self.binstableitemchanges: return bin_opacitieslst = eval(self.bin_opacities) nbins = len(bin_opacitieslst) sum = 0 for binopacity in bin_opacitieslst: sum += float(binopacity.split(",")[0]) if sum >= nbins: self.OpaqueAllCheckbox.setCheckState(Qt.Checked) if sum == 0: self.OpaqueAllCheckbox.setCheckState(Qt.Unchecked) if sum > 0.0 and sum < nbins: self.OpaqueAllCheckbox.setCheckState(Qt.PartiallyChecked) def onBinsTableItemChanged(self, item): row = item.row() column = item.column() try: if item.checkState() == Qt.Unchecked: newval = 0 else: newval = 1.0 if column == 3 and self.binstable_isready: # changing opacity assert (newval <= 1.0 and newval >= 0.0) bin_opacitieslst = eval(self.bin_opacities) bin_opacitieslst[row] = str(newval) + ', ' + str(row) self.bin_opacities = str(bin_opacitieslst) self.SetOpaqueAll() self.NGL_HKL_command( 'NGL_HKLviewer.viewer.NGL.bin_opacities = "%s"' % self.bin_opacities) except Exception as e: print(str(e)) #self.binstable.currentItem().setText( self.currentSelectedBinsTableVal) def onBinsTableItemSelectionChanged(self): row = self.binstable.currentItem().row() column = self.binstable.currentItem().column() self.currentSelectedBinsTableVal = self.binstable.currentItem().text() #print( "in itemSelectionChanged " + self.currentSelectedBinsTableVal) def onOpaqueAll(self): self.binstableitemchanges = True bin_opacitieslst = eval(self.bin_opacities) nbins = len(bin_opacitieslst) bin_opacitieslst = [] self.binstable_isready = False if self.OpaqueAllCheckbox.isChecked(): for i in range(nbins): bin_opacitieslst.append("1.0, %d" % i) else: for i in range(nbins): bin_opacitieslst.append("0.0, %d" % i) self.bin_opacities = str(bin_opacitieslst) self.NGL_HKL_command('NGL_HKLviewer.viewer.NGL.bin_opacities = "%s"' % self.bin_opacities) self.binstableitemchanges = False self.binstable_isready = True """ def onLoadFinished(self, val): pass #print("web page finished loading now") def onBinsTableitemActivated(self, item): row = item.row() column = item.column() currentval = item.text() #print( "in itemActivated " + currentval) def onBinsTableCellentered(self, row, col): pass #print( "in Cellentered " + self.binstable.currentItem().text() ) def onBinsTableCellPressed(self, row, col): pass #print( "in CellPressed " + self.binstable.currentItem().text() ) """ def onNbinsChanged(self, val): self.nbins = val if not self.updatingNbins: # avoid possible endless loop to cctbx self.NGL_HKL_command("NGL_HKLviewer.nbins = %d" % self.nbins) def onRadiiScaleChanged(self, val): self.radii_scale = val self.NGL_HKL_command(""" NGL_HKLviewer.viewer { nth_power_scale_radii = %f scale = %f } """ % (self.nth_power_scale, self.radii_scale)) def onPowerScaleChanged(self, val): self.nth_power_scale = val self.NGL_HKL_command(""" NGL_HKLviewer.viewer { nth_power_scale_radii = %f scale = %f } """ % (self.nth_power_scale, self.radii_scale)) def onManualPowerScale(self): if self.ManualPowerScalecheckbox.isChecked(): self.NGL_HKL_command( 'NGL_HKLviewer.viewer.nth_power_scale_radii = %f' % self.nth_power_scale) self.power_scale_spinBox.setEnabled(True) else: self.NGL_HKL_command( 'NGL_HKLviewer.viewer.nth_power_scale_radii = -1.0') self.power_scale_spinBox.setEnabled(False) self.nth_power_scale = -1.0 def OpenReflectionsFile(self): options = QFileDialog.Options() fileName, filtr = QFileDialog.getOpenFileName( self, "Load reflections file", "", "All Files (*);;MTZ Files (*.mtz);;CIF (*.cif)", "", options) if fileName: self.HKLnameedit.setText(fileName) #self.infostr = "" self.textInfo.setPlainText("") self.fileisvalid = False self.NGL_HKL_command('NGL_HKLviewer.filename = "%s"' % fileName) self.MillerComboBox.clear() self.BinDataComboBox.clear() def createExpansionBox(self): self.SpaceGroupComboBox = QComboBox() self.SpaceGroupComboBox.activated.connect(self.SpacegroupSelchange) self.SpacegroupLabel = QLabel() self.SpacegroupLabel.setText("Space Subgroups") self.mergecheckbox = QCheckBox() self.mergecheckbox.setText("Merge data") #self.mergecheckbox.setTristate (True) self.mergecheckbox.clicked.connect(self.MergeData) self.expandP1checkbox = QCheckBox() self.expandP1checkbox.setText("Expand to P1") self.expandP1checkbox.clicked.connect(self.ExpandToP1) self.expandAnomalouscheckbox = QCheckBox() self.expandAnomalouscheckbox.setText("Show Friedel pairs") self.expandAnomalouscheckbox.clicked.connect(self.ExpandAnomalous) self.sysabsentcheckbox = QCheckBox() self.sysabsentcheckbox.setText("Show Systematic Absences") self.sysabsentcheckbox.clicked.connect(self.showSysAbsent) self.missingcheckbox = QCheckBox() self.missingcheckbox.setText("Show Missing") self.missingcheckbox.clicked.connect(self.showMissing) self.onlymissingcheckbox = QCheckBox() self.onlymissingcheckbox.setText("Only Show Missing") self.onlymissingcheckbox.clicked.connect(self.showOnlyMissing) self.ExpansionBox = QGroupBox("Expansions") layout = QGridLayout() layout.addWidget(self.SpacegroupLabel, 0, 0) layout.addWidget(self.SpaceGroupComboBox, 0, 1) #layout.addWidget(self.mergecheckbox, 1, 0) layout.addWidget(self.expandP1checkbox, 1, 0) layout.addWidget(self.expandAnomalouscheckbox, 1, 1) layout.addWidget(self.sysabsentcheckbox, 2, 0) layout.addWidget(self.missingcheckbox, 3, 0) layout.addWidget(self.onlymissingcheckbox, 3, 1) layout.setRowStretch(0, 0) layout.setRowStretch(1, 0) layout.setRowStretch(2, 0) layout.setRowStretch(3, 1) self.ExpansionBox.setLayout(layout) def CreateSliceTabs(self): self.showslicecheckbox = QCheckBox() self.showslicecheckbox.setText("Show Slice") self.showslicecheckbox.clicked.connect(self.showSlice) self.sliceindexspinBox = QDoubleSpinBox() self.sliceindex = 0 self.sliceindexspinBox.setValue(self.sliceindex) self.sliceindexspinBox.setDecimals(0) self.sliceindexspinBox.setSingleStep(1) self.sliceindexspinBox.setRange(0, 20) self.sliceindexspinBox.valueChanged.connect(self.onSliceIndexChanged) self.SliceLabelComboBox = QComboBox() self.SliceLabelComboBox.activated.connect(self.onSliceComboSelchange) self.sliceaxis = ["h", "k", "l"] self.SliceLabelComboBox.addItems(self.sliceaxis) self.sliceTabWidget = QTabWidget() tab1 = QWidget() layout1 = QGridLayout() layout1.addWidget(self.showslicecheckbox, 0, 0, 1, 1) layout1.addWidget(self.SliceLabelComboBox, 0, 1, 1, 1) layout1.addWidget(self.sliceindexspinBox, 0, 2, 1, 1) tab1.setLayout(layout1) tab2 = QWidget() layout2 = QGridLayout() self.hvec_spinBox = QDoubleSpinBox(self.sliceTabWidget) self.hvecval = 2.0 self.hvec_spinBox.setValue(self.hvecval) self.hvec_spinBox.setDecimals(2) self.hvec_spinBox.setSingleStep(0.5) self.hvec_spinBox.setRange(-100.0, 10.0) self.hvec_spinBox.valueChanged.connect(self.onHvecChanged) self.hvec_Label = QLabel() self.hvec_Label.setText("H") layout2.addWidget(self.hvec_Label, 0, 0, 1, 1) layout2.addWidget(self.hvec_spinBox, 0, 1, 1, 1) self.kvec_spinBox = QDoubleSpinBox(self.sliceTabWidget) self.kvecval = 0.0 self.kvec_spinBox.setValue(self.kvecval) self.kvec_spinBox.setDecimals(2) self.kvec_spinBox.setSingleStep(0.5) self.kvec_spinBox.setRange(-100.0, 100.0) self.kvec_spinBox.valueChanged.connect(self.onKvecChanged) self.kvec_Label = QLabel() self.kvec_Label.setText("K") layout2.addWidget(self.kvec_Label, 1, 0, 1, 1) layout2.addWidget(self.kvec_spinBox, 1, 1, 1, 1) self.lvec_spinBox = QDoubleSpinBox(self.sliceTabWidget) self.lvecval = 0.0 self.lvec_spinBox.setValue(self.lvecval) self.lvec_spinBox.setDecimals(2) self.lvec_spinBox.setSingleStep(0.5) self.lvec_spinBox.setRange(-100.0, 100.0) self.lvec_spinBox.valueChanged.connect(self.onLvecChanged) self.lvec_Label = QLabel() self.lvec_Label.setText("L") layout2.addWidget(self.lvec_Label, 2, 0, 1, 1) layout2.addWidget(self.lvec_spinBox, 2, 1, 1, 1) self.hkldist_spinBox = QDoubleSpinBox(self.sliceTabWidget) self.hkldistval = 0.0 self.hkldist_spinBox.setValue(self.hkldistval) self.hkldist_spinBox.setDecimals(2) self.hkldist_spinBox.setSingleStep(0.5) self.hkldist_spinBox.setRange(-100.0, 100.0) self.hkldist_spinBox.valueChanged.connect(self.onHKLdistChanged) self.hkldist_Label = QLabel() self.hkldist_Label.setText("Distance from Origin") layout2.addWidget(self.hkldist_Label, 3, 0, 1, 1) layout2.addWidget(self.hkldist_spinBox, 3, 1, 1, 1) self.clipwidth_spinBox = QDoubleSpinBox(self.sliceTabWidget) self.clipwidthval = 0.5 self.clipwidth_spinBox.setValue(self.clipwidthval) self.clipwidth_spinBox.setDecimals(2) self.clipwidth_spinBox.setSingleStep(0.05) self.clipwidth_spinBox.setRange(0.0, 100.0) self.clipwidth_spinBox.valueChanged.connect(self.onClipwidthChanged) self.clipwidth_Label = QLabel() self.clipwidth_Label.setText("Clip Plane Width") layout2.addWidget(self.clipwidth_Label, 4, 0, 1, 1) layout2.addWidget(self.clipwidth_spinBox, 4, 1, 1, 1) self.ClipBox = QGroupBox("Normal Vector to Clip Plane") self.ClipBox.setLayout(layout2) layout3 = QGridLayout() self.ClipPlaneChkBox = QCheckBox(self.sliceTabWidget) self.ClipPlaneChkBox.setText( "Use clip plane normal to HKL vector pointing out") self.ClipPlaneChkBox.clicked.connect(self.onClipPlaneChkBox) layout3.addWidget(self.ClipPlaneChkBox, 0, 0) layout3.addWidget(self.ClipBox, 1, 0) tab2.setLayout(layout3) self.sliceTabWidget.addTab(tab1, "Explicit Slicing") self.sliceTabWidget.addTab(tab2, "Clip Plane Slicing") self.ClipBox.setDisabled(True) def onClipPlaneChkBox(self): if self.ClipPlaneChkBox.isChecked(): self.ClipBox.setDisabled(False) philstr = """NGL_HKLviewer.normal_clip_plane { h = %s k = %s l = %s hkldist = %s clipwidth = %s } NGL_HKLviewer.viewer.NGL.fixorientation = %s """ %(self.hvecval, self.kvecval, self.lvecval, self.hkldistval, self.clipwidthval, \ str(self.fixedorientcheckbox.isChecked()) ) self.NGL_HKL_command(philstr) else: self.ClipBox.setDisabled(True) self.NGL_HKL_command( "NGL_HKLviewer.normal_clip_plane.clipwidth = None") def onClipwidthChanged(self, val): self.clipwidthval = val self.NGL_HKL_command("NGL_HKLviewer.normal_clip_plane.clipwidth = %f" % self.clipwidthval) def onHKLdistChanged(self, val): self.hkldistval = val self.NGL_HKL_command("NGL_HKLviewer.normal_clip_plane.hkldist = %f" % self.hkldistval) def onHvecChanged(self, val): self.hvecval = val self.NGL_HKL_command("NGL_HKLviewer.normal_clip_plane.h = %f" % self.hvecval) def onKvecChanged(self, val): self.kvecval = val self.NGL_HKL_command("NGL_HKLviewer.normal_clip_plane.k = %f" % self.kvecval) def onLvecChanged(self, val): self.lvecval = val self.NGL_HKL_command("NGL_HKLviewer.normal_clip_plane.l = %f" % self.lvecval) def onFixedorient(self): self.NGL_HKL_command('NGL_HKLviewer.viewer.NGL.fixorientation = %s' \ %str(self.fixedorientcheckbox.isChecked())) def onMillerComboSelchange(self, i): self.NGL_HKL_command("NGL_HKLviewer.scene_id = %d" % i) #self.MillerComboBox.setCurrentIndex(i) if self.MillerComboBox.currentText(): self.functionTabWidget.setEnabled(True) self.expandAnomalouscheckbox.setEnabled(True) # don' allow anomalous expansion for data that's already anomalous for arrayinfo in self.array_infotpls: isanomalous = arrayinfo[-1] label = arrayinfo[0] if isanomalous and label == self.MillerComboBox.currentText( )[:len(label)]: self.expandAnomalouscheckbox.setDisabled(True) else: self.functionTabWidget.setDisabled(True) self.SpaceGroupComboBox.clear() self.SpaceGroupComboBox.addItems(self.spacegroups) # need to supply issymunique flag in infotuple #if self.hklscenes_arrays[ i ][6] == 0: # self.mergecheckbox.setEnabled(True) #else: # self.mergecheckbox.setEnabled(False) def createFileInfoBox(self): self.FileInfoBox = QGroupBox("Reflection File Information") layout = QGridLayout() layout.addWidget(self.openFileNameButton, 0, 0, 1, 2) if self.devmode: layout.addWidget(self.debugbutton, 0, 2, 1, 1) layout.addWidget(self.HKLnameedit, 1, 0, 1, 3) layout.addWidget(self.millertable, 2, 0, 1, 3) layout.addWidget(self.textInfo, 3, 0, 1, 3) #layout.setColumnStretch(1, 2) self.FileInfoBox.setLayout(layout) def createRadiiScaleGroupBox(self): self.RadiiScaleGroupBox = QGroupBox("Radii Size of HKL Spheres") self.ManualPowerScalecheckbox = QCheckBox() self.ManualPowerScalecheckbox.setText( "Manual Power Scaling of Sphere Radii") self.ManualPowerScalecheckbox.clicked.connect(self.onManualPowerScale) self.power_scale_spinBox = QDoubleSpinBox(self.RadiiScaleGroupBox) self.nth_power_scale = 0.5 self.power_scale_spinBox.setValue(self.nth_power_scale) self.power_scale_spinBox.setDecimals(2) self.power_scale_spinBox.setSingleStep(0.05) self.power_scale_spinBox.setRange(0.0, 1.0) self.power_scale_spinBox.valueChanged.connect(self.onPowerScaleChanged) self.power_scale_spinBox.setEnabled(False) self.powerscaleLabel = QLabel() self.powerscaleLabel.setText("Power scale Factor") self.radii_scale_spinBox = QDoubleSpinBox(self.RadiiScaleGroupBox) self.radii_scale = 1.0 self.radii_scale_spinBox.setValue(self.radii_scale) self.radii_scale_spinBox.setDecimals(1) self.radii_scale_spinBox.setSingleStep(0.1) self.radii_scale_spinBox.setRange(0.2, 2.0) self.radii_scale_spinBox.valueChanged.connect(self.onRadiiScaleChanged) self.radiiscaleLabel = QLabel() self.radiiscaleLabel.setText("Linear Scale Factor") layout = QGridLayout() layout.addWidget(self.ManualPowerScalecheckbox, 1, 0, 1, 2) layout.addWidget(self.powerscaleLabel, 2, 0, 1, 2) layout.addWidget(self.power_scale_spinBox, 2, 1, 1, 2) layout.addWidget(self.radiiscaleLabel, 3, 0, 1, 2) layout.addWidget(self.radii_scale_spinBox, 3, 1, 1, 2) layout.setColumnStretch(0, 1) layout.setColumnStretch(1, 0) self.RadiiScaleGroupBox.setLayout(layout) def createBinsBox(self): self.binstable = QTableWidget(0, 4) self.binstable_isready = False labels = [ "no. of HKLs", "lower bin value", "upper bin value", "opacity" ] self.binstable.setHorizontalHeaderLabels(labels) self.binstable.horizontalHeader().setDefaultAlignment(Qt.AlignLeft) self.bindata_labeltxt = QLabel() self.bindata_labeltxt.setText("Data binned:") self.Nbins_spinBox = QSpinBox() self.Nbins_spinBox.setSingleStep(1) self.Nbins_spinBox.setRange(1, 40) self.Nbins_spinBox.valueChanged.connect(self.onNbinsChanged) self.Nbins_labeltxt = QLabel() self.Nbins_labeltxt.setText("Number of bins:") self.OpaqueAllCheckbox = QCheckBox() #self.OpaqueAllCheckbox.setTristate() self.OpaqueAllCheckbox.setText("Show all data in bins") self.OpaqueAllCheckbox.clicked.connect(self.onOpaqueAll) self.binstable.itemChanged.connect(self.onBinsTableItemChanged) self.binstable.itemSelectionChanged.connect( self.onBinsTableItemSelectionChanged) self.BinDataComboBox = QComboBox() self.BinDataComboBox.activated.connect(self.onBindataComboSelchange) self.BinsGroupBox = QGroupBox("Bins") layout = QGridLayout() layout.addWidget(self.bindata_labeltxt, 0, 0) layout.addWidget(self.BinDataComboBox, 0, 1) layout.addWidget(self.Nbins_labeltxt, 0, 2) layout.addWidget(self.Nbins_spinBox, 0, 3) layout.addWidget(self.OpaqueAllCheckbox, 1, 2) layout.addWidget(self.binstable, 2, 0, 1, 4) layout.setColumnStretch(0, 0) layout.setColumnStretch(1, 2) layout.setColumnStretch(3, 1) self.BinsGroupBox.setLayout(layout) def DebugInteractively(self): import code, traceback code.interact(local=locals(), banner="".join(traceback.format_stack(limit=10))) def CreateFunctionTabs(self): self.functionTabWidget = QTabWidget() tab1 = QWidget() layout1 = QGridLayout() layout1.addWidget(self.ExpansionBox, 0, 0) layout1.setRowStretch(0, 0) tab1.setLayout(layout1) tab2 = QWidget() layout2 = QGridLayout() self.fixedorientcheckbox = QCheckBox(self.sliceTabWidget) self.fixedorientcheckbox.setText( "Fix orientation but allow zoom and translation") self.fixedorientcheckbox.clicked.connect(self.onFixedorient) layout2.addWidget(self.fixedorientcheckbox, 0, 0) layout2.addWidget(self.sliceTabWidget, 1, 0) tab2.setLayout(layout2) tab3 = QWidget() layout3 = QGridLayout() layout3.addWidget(self.RadiiScaleGroupBox, 0, 0) tab3.setLayout(layout3) tab4 = QWidget() layout4 = QGridLayout() layout4.addWidget(self.BinsGroupBox, 0, 0) tab4.setLayout(layout4) self.functionTabWidget.addTab(tab1, "Expand") self.functionTabWidget.addTab(tab2, "Slice") self.functionTabWidget.addTab(tab3, "Size") self.functionTabWidget.addTab(tab4, "Bins") self.functionTabWidget.setDisabled(True) def SpacegroupSelchange(self, i): self.NGL_HKL_command("NGL_HKLviewer.spacegroup_choice = %d" % i) def find_free_port(self): import socket s = socket.socket() s.bind(('', 0)) # Bind to a free port provided by the host. port = s.getsockname()[1] s.close() return port def LaunchCCTBXPython(self): self.sockport = self.find_free_port() self.zmq_context = zmq.Context() self.socket = self.zmq_context.socket(zmq.PAIR) self.socket.bind("tcp://127.0.0.1:%s" % self.sockport) try: msg = self.socket.recv( flags=zmq.NOBLOCK) #To empty the socket from previous messages except Exception as e: pass cmdargs = 'cctbx.python.bat -i -c "from crys3d.hklview import cmdlineframes;' \ + ' myHKLview = cmdlineframes.HKLViewFrame(useGuiSocket=%s, high_quality=True,' %self.sockport \ + ' jscriptfname = \'%s\', ' %self.jscriptfname \ + ' verbose=%s, UseOSBrowser= %s )"\n' %(self.verbose, str(self.UseOSbrowser)) self.cctbxproc = subprocess.Popen(cmdargs, shell=True, stdin=subprocess.PIPE, stdout=sys.stdout, stderr=sys.stderr) #time.sleep(1) def NGL_HKL_command(self, cmdstr): #print("sending:\n" + cmdstr) self.socket.send(bytes(cmdstr, "utf-8"))
class HistWidget(ToolWidget): def __init__(self, image, parent=None): super(ToolWidget, self).__init__(parent) self.value_radio = QRadioButton(self.tr('Value')) self.value_radio.setChecked(True) self.last_radio = self.value_radio self.red_radio = QRadioButton(self.tr('Red')) self.green_radio = QRadioButton(self.tr('Green')) self.blue_radio = QRadioButton(self.tr('Blue')) self.rgb_radio = QRadioButton(self.tr('RGB')) self.smooth_check = QCheckBox(self.tr('Smooth line')) self.smooth_check.setToolTip(self.tr('Interpolated values plot')) self.log_check = QCheckBox(self.tr('Log scale')) self.log_check.setToolTip(self.tr('Y-axes logarithmic scale')) self.grid_check = QCheckBox(self.tr('Show grid')) self.grid_check.setToolTip(self.tr('Display XY main grid lines')) self.marker_check = QCheckBox(self.tr('Show markers')) self.marker_check.setToolTip( self.tr('Show plot markers for min(--), avg(-), max(-.)')) self.start_slider = ParamSlider([0, 255], 8, 0, bold=True) self.end_slider = ParamSlider([0, 255], 8, 255, bold=True) channels = cv.split(cv.cvtColor(image, cv.COLOR_BGR2RGB)) channels.append(cv.cvtColor(image, cv.COLOR_BGR2GRAY)) self.hist = [compute_hist(c) for c in channels] rows, cols, chans = image.shape pixels = rows * cols self.unique_colors = np.unique(np.reshape(image, (pixels, chans)), axis=0).shape[0] self.unique_ratio = np.round(self.unique_colors / pixels * 100, 2) self.value_radio.clicked.connect(self.redraw) self.red_radio.clicked.connect(self.redraw) self.green_radio.clicked.connect(self.redraw) self.blue_radio.clicked.connect(self.redraw) self.rgb_radio.clicked.connect(self.redraw) self.smooth_check.stateChanged.connect(self.redraw) self.log_check.stateChanged.connect(self.redraw) self.grid_check.stateChanged.connect(self.redraw) self.marker_check.stateChanged.connect(self.redraw) self.start_slider.valueChanged.connect(self.redraw) self.end_slider.valueChanged.connect(self.redraw) self.table_widget = QTableWidget(13, 2) self.table_widget.setHorizontalHeaderLabels( [self.tr('Property'), self.tr('Value')]) self.table_widget.setItem(0, 0, QTableWidgetItem(self.tr('Least frequent'))) self.table_widget.item(0, 0).setToolTip( self.tr('Value that appears less')) self.table_widget.setItem(1, 0, QTableWidgetItem(self.tr('Most frequent'))) self.table_widget.item(1, 0).setToolTip( self.tr('Value that appears more')) self.table_widget.setItem(2, 0, QTableWidgetItem(self.tr('Average level'))) self.table_widget.item(2, 0).setToolTip(self.tr('Histogram mean value')) self.table_widget.setItem(3, 0, QTableWidgetItem(self.tr('Median level'))) self.table_widget.item(3, 0).setToolTip(self.tr('Histogram median value')) self.table_widget.setItem(4, 0, QTableWidgetItem(self.tr('Deviation'))) self.table_widget.item(4, 0).setToolTip( self.tr('Histogram standard deviation')) self.table_widget.setItem(5, 0, QTableWidgetItem(self.tr('Pixel count'))) self.table_widget.item(5, 0).setToolTip( self.tr('Total values in current range')) self.table_widget.setItem(6, 0, QTableWidgetItem(self.tr('Percentile'))) self.table_widget.item(6, 0).setToolTip( self.tr('Percentage of total pixels')) self.table_widget.setItem(7, 0, QTableWidgetItem(self.tr('Nonzero range'))) self.table_widget.item(7, 0).setToolTip( self.tr('Minimal range without empty bins')) self.table_widget.setItem(8, 0, QTableWidgetItem(self.tr('Empty bins'))) self.table_widget.item(8, 0).setToolTip( self.tr('Number of missing values')) self.table_widget.setItem(9, 0, QTableWidgetItem(self.tr('Unique colors'))) self.table_widget.item(9, 0).setToolTip(self.tr('Unique RGB color count')) self.table_widget.setItem(10, 0, QTableWidgetItem(self.tr('Unique ratio'))) self.table_widget.item(10, 0).setToolTip( self.tr('Unique colors vs total pixels')) self.table_widget.setItem(11, 0, QTableWidgetItem(self.tr('Smoothness'))) self.table_widget.item(11, 0).setToolTip( self.tr('Estimated correlation among bin values')) self.table_widget.setItem(12, 0, QTableWidgetItem(self.tr('Fullness'))) self.table_widget.item(12, 0).setToolTip( self.tr('Area covered vs total size')) for i in range(self.table_widget.rowCount()): modify_font(self.table_widget.item(i, 0), bold=True) self.table_widget.setSelectionMode(QAbstractItemView.SingleSelection) self.table_widget.setEditTriggers(QAbstractItemView.NoEditTriggers) self.table_widget.setAlternatingRowColors(True) self.table_widget.setMinimumWidth(200) self.table_widget.resizeColumnsToContents() figure = Figure() plot_canvas = FigureCanvas(figure) self.axes = plot_canvas.figure.subplots() self.redraw() figure.set_tight_layout(True) range_layout = QGridLayout() range_layout.addWidget(QLabel(self.tr('Start:')), 0, 0) range_layout.addWidget(self.start_slider, 0, 1) range_layout.addWidget(QLabel(self.tr('End:')), 1, 0) range_layout.addWidget(self.end_slider, 1, 1) right_frame = QFrame() right_layout = QVBoxLayout() right_layout.addWidget(self.table_widget) right_layout.addLayout(range_layout) right_frame.setLayout(right_layout) center_split = QSplitter() center_split.addWidget(plot_canvas) center_split.addWidget(right_frame) bottom_layout = QHBoxLayout() bottom_layout.addWidget(QLabel(self.tr('Channel:'))) bottom_layout.addWidget(self.value_radio) bottom_layout.addWidget(self.red_radio) bottom_layout.addWidget(self.green_radio) bottom_layout.addWidget(self.blue_radio) bottom_layout.addWidget(self.rgb_radio) bottom_layout.addStretch() bottom_layout.addWidget(self.smooth_check) bottom_layout.addWidget(self.log_check) bottom_layout.addWidget(self.grid_check) bottom_layout.addWidget(self.marker_check) main_layout = QVBoxLayout() main_layout.addWidget(center_split) main_layout.addLayout(bottom_layout) self.setLayout(main_layout) def redraw(self): x = np.arange(256) alpha = 0.25 rgb = self.rgb_radio.isChecked() red = self.red_radio.isChecked() green = self.green_radio.isChecked() blue = self.blue_radio.isChecked() value = self.value_radio.isChecked() smoothness = self.smooth_check.isChecked() grid = self.grid_check.isChecked() log = self.log_check.isChecked() try: self.axes.clear() except RecursionError: return y = None step = None if smoothness else 'mid' if value: y = self.hist[3] if smoothness: self.axes.plot(x, y, 'k') else: self.axes.step(x, y, 'k', where='mid') self.axes.fill_between(x, y, alpha=alpha, facecolor='k', step=step) else: if red or rgb: y = self.hist[0] if smoothness: self.axes.plot(x, y, 'r') else: self.axes.step(x, y, 'r', where='mid') self.axes.fill_between(x, y, alpha=alpha, facecolor='r', step=step) if green or rgb: y = self.hist[1] if smoothness: self.axes.plot(x, y, 'g') else: self.axes.step(x, y, 'g', where='mid') self.axes.fill_between(x, y, alpha=alpha, facecolor='g', step=step) if blue or rgb: y = self.hist[2] if smoothness: self.axes.plot(x, y, 'b') else: self.axes.step(x, y, 'b', where='mid') self.axes.fill_between(x, y, alpha=alpha, facecolor='b', step=step) if log: self.axes.set_yscale('log') self.axes.set_ylim(bottom=1) else: self.axes.set_yscale('linear') self.axes.set_ylim(bottom=0) self.axes.set_xlim([0, 255]) self.axes.set_xlabel(self.tr('intensity value')) self.axes.set_ylabel(self.tr('pixel count')) self.axes.set_xticks([0, 64, 128, 192, 255]) self.axes.grid(grid, which='both') if rgb: self.table_widget.setEnabled(False) self.marker_check.setEnabled(False) self.start_slider.setEnabled(False) self.end_slider.setEnabled(False) for i in range(self.table_widget.rowCount()): if self.table_widget.item(i, 1) is not None: self.table_widget.item(i, 1).setText('') self.table_widget.item(i, 1).setBackgroundColor( QColor('white')) else: self.table_widget.setEnabled(True) self.marker_check.setEnabled(True) self.start_slider.setEnabled(True) self.end_slider.setEnabled(True) start = self.start_slider.value() end = self.end_slider.value() if end <= start: end = start + 1 elif start >= end: start = end - 1 total = np.sum(y) x = x[start:end + 1] y = y[start:end + 1] count = np.sum(y) if count != 0: argmin = np.argmin(y) + start argmax = np.argmax(y) + start mean = np.round(np.sum(x * y) / count, 2) stddev = np.round(np.sqrt(np.sum(((x - mean)**2) * y) / count), 2) median = np.argmax(np.cumsum(y) > count / 2) + start percent = np.round(count / total * 100, 2) empty = len(x) - np.count_nonzero(y) nonzero = [ np.nonzero(y)[0][0] + start, np.nonzero(y)[0][-1] + start ] fullness = np.round(count / (255 * np.max(y)) * 100, 2) y = y / np.max(y) sweep = len(y) smoothness = 0 if sweep >= 5: for i in range(2, sweep - 2): yl = 2 * y[i - 1] - y[i - 2] yr = 2 * y[i + 1] - y[i + 2] smoothness += abs(y[i] - (yl + yr) / 2) smoothness = np.round( (1 - (smoothness / (sweep - 2))) * 100, 2) if self.marker_check.isChecked(): self.axes.axvline(argmin, linestyle='--', color='m') self.axes.axvline(mean, linestyle='-', color='m') self.axes.axvline(argmax, linestyle='-.', color='m') self.axes.axvline(median, linestyle=':', color='m') else: argmin = argmax = mean = stddev = median = percent = smoothness = empty = nonzero = fullness = 0 self.table_widget.setItem(0, 1, QTableWidgetItem(str(argmin))) self.table_widget.setItem(1, 1, QTableWidgetItem(str(argmax))) self.table_widget.setItem(2, 1, QTableWidgetItem(str(mean))) self.table_widget.setItem(3, 1, QTableWidgetItem(str(median))) self.table_widget.setItem(4, 1, QTableWidgetItem(str(stddev))) self.table_widget.setItem(5, 1, QTableWidgetItem(str(count))) self.table_widget.setItem(6, 1, QTableWidgetItem(str(percent) + '%')) self.table_widget.setItem(7, 1, QTableWidgetItem(str(nonzero))) self.table_widget.setItem(8, 1, QTableWidgetItem(str(empty))) self.table_widget.setItem( 9, 1, QTableWidgetItem(str(self.unique_colors))) self.table_widget.setItem( 10, 1, QTableWidgetItem(str(self.unique_ratio) + '%')) color_by_value(self.table_widget.item(10, 1), self.unique_ratio, [25, 50, 75]) self.table_widget.setItem(11, 1, QTableWidgetItem(str(smoothness) + '%')) color_by_value(self.table_widget.item(11, 1), smoothness, [80, 90, 95]) self.table_widget.setItem(12, 1, QTableWidgetItem(str(fullness) + '%')) color_by_value(self.table_widget.item(12, 1), fullness, [5, 10, 20]) self.table_widget.resizeColumnsToContents() if start != 0 or end != 255: self.axes.axvline(start, linestyle=':', color='k') self.axes.axvline(end, linestyle=':', color='k') _, top = self.axes.get_ylim() self.axes.fill_between(np.arange(start, end + 1), top, facecolor='y', alpha=alpha * 2) self.axes.figure.canvas.draw()
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 View(QMainWindow): def __init__(self, model, controller): super().__init__() self._model = model self._controller = controller self.segmentcursor = False self.togglecolors = {"#1f77b4": "m", "m": "#1f77b4"} ################################################################# # define GUI layout and connect input widgets to external slots # ################################################################# self.setWindowTitle("biopeaks") self.setGeometry(50, 50, 1750, 750) self.setWindowIcon(QIcon(":/python_icon.png")) # figure0 for signal self.figure0 = Figure() self.canvas0 = FigureCanvas(self.figure0) # Enforce minimum height, otherwise resizing with self.splitter causes # mpl to throw an error because figure is resized to height 0. The # widget can still be fully collapsed with self.splitter- self.canvas0.setMinimumHeight(1) # in pixels self.ax00 = self.figure0.add_subplot(1, 1, 1) self.ax00.set_frame_on(False) self.figure0.subplots_adjust(left=0.04, right=0.98, bottom=0.25) self.line00 = None self.scat = None self.segmentspan = None # figure1 for marker self.figure1 = Figure() self.canvas1 = FigureCanvas(self.figure1) self.canvas1.setMinimumHeight(1) self.ax10 = self.figure1.add_subplot(1, 1, 1, sharex=self.ax00) self.ax10.get_xaxis().set_visible(False) self.ax10.set_frame_on(False) self.figure1.subplots_adjust(left=0.04, right=0.98) self.line10 = None # figure2 for statistics self.figure2 = Figure() self.canvas2 = FigureCanvas(self.figure2) self.canvas2.setMinimumHeight(1) self.ax20 = self.figure2.add_subplot(3, 1, 1, sharex=self.ax00) self.ax20.get_xaxis().set_visible(False) self.ax20.set_frame_on(False) self.line20 = None self.ax21 = self.figure2.add_subplot(3, 1, 2, sharex=self.ax00) self.ax21.get_xaxis().set_visible(False) self.ax21.set_frame_on(False) self.line21 = None self.ax22 = self.figure2.add_subplot(3, 1, 3, sharex=self.ax00) self.ax22.get_xaxis().set_visible(False) self.ax22.set_frame_on(False) self.line22 = None self.figure2.subplots_adjust(left=0.04, right=0.98) # navigation bar self.navitools = CustomNavigationToolbar(self.canvas0, self) # peak editing self.editcheckbox = QCheckBox("editable", self) self.editcheckbox.stateChanged.connect(self._model.set_peakseditable) # peak saving batch self.savecheckbox = QCheckBox("save during batch processing", self) self.savecheckbox.stateChanged.connect(self._model.set_savebatchpeaks) # peak auto-correction batch self.correctcheckbox = QCheckBox("correct during batch processing", self) self.correctcheckbox.stateChanged.connect( self._model.set_correctbatchpeaks) # selecting stats for saving self.periodcheckbox = QCheckBox("period", self) self.periodcheckbox.stateChanged.connect( lambda: self.select_stats("period")) self.ratecheckbox = QCheckBox("rate", self) self.ratecheckbox.stateChanged.connect( lambda: self.select_stats("rate")) self.tidalampcheckbox = QCheckBox("tidal amplitude", self) self.tidalampcheckbox.stateChanged.connect( lambda: self.select_stats("tidalamp")) # channel selection self.sigchanmenulabel = QLabel("biosignal") self.sigchanmenu = QComboBox(self) self.sigchanmenu.addItem("A1") self.sigchanmenu.addItem("A2") self.sigchanmenu.addItem("A3") self.sigchanmenu.addItem("A4") self.sigchanmenu.addItem("A5") self.sigchanmenu.addItem("A6") self.sigchanmenu.currentTextChanged.connect(self._model.set_signalchan) # initialize with default value self._model.set_signalchan(self.sigchanmenu.currentText()) self.markerchanmenulabel = QLabel("marker") self.markerchanmenu = QComboBox(self) self.markerchanmenu.addItem("none") self.markerchanmenu.addItem("I1") self.markerchanmenu.addItem("I2") self.markerchanmenu.addItem("A1") self.markerchanmenu.addItem("A2") self.markerchanmenu.addItem("A3") self.markerchanmenu.addItem("A4") self.markerchanmenu.addItem("A5") self.markerchanmenu.addItem("A6") self.markerchanmenu.currentTextChanged.connect( self._model.set_markerchan) # initialize with default value self._model.set_markerchan(self.markerchanmenu.currentText()) # processing mode (batch or single file) self.batchmenulabel = QLabel("mode") self.batchmenu = QComboBox(self) self.batchmenu.addItem("single file") self.batchmenu.addItem("multiple files") self.batchmenu.currentTextChanged.connect(self._model.set_batchmode) self.batchmenu.currentTextChanged.connect(self.toggle_options) # initialize with default value self._model.set_batchmode(self.batchmenu.currentText()) self.toggle_options(self.batchmenu.currentText()) # modality selection self.modmenulabel = QLabel("modality") self.modmenu = QComboBox(self) self.modmenu.addItem("ECG") self.modmenu.addItem("PPG") self.modmenu.addItem("RESP") self.modmenu.currentTextChanged.connect(self._model.set_modality) self.modmenu.currentTextChanged.connect(self.toggle_options) # initialize with default value self._model.set_modality(self.modmenu.currentText()) self.toggle_options(self.modmenu.currentText()) # segment selection; this widget can be openend / set visible from # the menu and closed from within itself (see mapping of segmentermap); # it provides utilities to select a segment from the signal self.segmentermap = QSignalMapper(self) self.segmenter = QDockWidget("select a segment", self) # disable closing such that widget can only be closed by confirming # selection or custom button self.segmenter.setFeatures(QDockWidget.NoDockWidgetFeatures) # Limit number of decimals to four. regex = QRegExp("[0-9]*\.?[0-9]{4}") validator = QRegExpValidator(regex) self.startlabel = QLabel("start") self.startedit = QLineEdit() self.startedit.setValidator(validator) self.endlabel = QLabel("end") self.endedit = QLineEdit() self.endedit.setValidator(validator) segmentfromcursor = QAction(QIcon(":/mouse_icon.png"), "select with mouse", self) segmentfromcursor.triggered.connect(self.enable_segmentedit) self.startedit.addAction(segmentfromcursor, QLineEdit.TrailingPosition) self.endedit.addAction(segmentfromcursor, QLineEdit.TrailingPosition) self.previewedit = QPushButton("preview segment") lambdafn = lambda: self._model.set_segment( [self.startedit.text(), self.endedit.text()]) self.previewedit.clicked.connect(lambdafn) self.confirmedit = QPushButton("confirm segment") self.confirmedit.clicked.connect(self._controller.segment_signal) self.confirmedit.clicked.connect(self.segmentermap.map) self.segmentermap.setMapping(self.confirmedit, 0) self.abortedit = QPushButton("abort segmentation") self.abortedit.clicked.connect(self.segmentermap.map) # reset the segment to None self.segmentermap.setMapping(self.abortedit, 2) self.segmenterlayout = QFormLayout() self.segmenterlayout.addRow(self.startlabel, self.startedit) self.segmenterlayout.addRow(self.endlabel, self.endedit) self.segmenterlayout.addRow(self.previewedit) self.segmenterlayout.addRow(self.confirmedit) self.segmenterlayout.addRow(self.abortedit) self.segmenterwidget = QWidget() self.segmenterwidget.setLayout(self.segmenterlayout) self.segmenter.setWidget(self.segmenterwidget) self.segmenter.setVisible(False) self.segmenter.setAllowedAreas(Qt.RightDockWidgetArea) self.addDockWidget(Qt.RightDockWidgetArea, self.segmenter) # Set up dialog to gather user input for custom files. regex = QRegExp("[1-9][0-9]") validator = QRegExpValidator(regex) self.signallabel = QLabel("biosignal column") self.signaledit = QLineEdit() self.signaledit.setValidator(validator) self.markerlabel = QLabel("marker column") self.markeredit = QLineEdit() self.markeredit.setValidator(validator) regex = QRegExp("[0-9]{2}") validator = QRegExpValidator(regex) self.headerrowslabel = QLabel("number of header rows") self.headerrowsedit = QLineEdit() self.headerrowsedit.setValidator(validator) regex = QRegExp("[0-9]{5}") validator = QRegExpValidator(regex) self.sfreqlabel = QLabel("sampling rate") self.sfreqedit = QLineEdit() self.sfreqedit.setValidator(validator) self.separatorlabel = QLabel("column separator") self.separatormenu = QComboBox(self) self.separatormenu.addItem("comma") self.separatormenu.addItem("tab") self.separatormenu.addItem("colon") self.separatormenu.addItem("space") self.continuecustomfile = QPushButton("continue loading file") self.continuecustomfile.clicked.connect(self.set_customheader) self.customfiledialog = QDialog() self.customfiledialog.setWindowTitle("custom file info") self.customfiledialog.setWindowIcon(QIcon(":/file_icon.png")) self.customfiledialog.setWindowFlags( Qt.WindowCloseButtonHint ) # remove help button by only setting close button self.customfilelayout = QFormLayout() self.customfilelayout.addRow(self.signallabel, self.signaledit) self.customfilelayout.addRow(self.markerlabel, self.markeredit) self.customfilelayout.addRow(self.separatorlabel, self.separatormenu) self.customfilelayout.addRow(self.headerrowslabel, self.headerrowsedit) self.customfilelayout.addRow(self.sfreqlabel, self.sfreqedit) self.customfilelayout.addRow(self.continuecustomfile) self.customfiledialog.setLayout(self.customfilelayout) # set up menubar menubar = self.menuBar() # signal menu signalmenu = menubar.addMenu("biosignal") openSignal = signalmenu.addMenu("load") openEDF = QAction("EDF", self) openEDF.triggered.connect(lambda: self._model.set_filetype("EDF")) openEDF.triggered.connect(self._controller.get_fpaths) openSignal.addAction(openEDF) openOpenSignals = QAction("OpenSignals", self) openOpenSignals.triggered.connect( lambda: self._model.set_filetype("OpenSignals")) openOpenSignals.triggered.connect(self._controller.get_fpaths) openSignal.addAction(openOpenSignals) openCustom = QAction("Custom", self) openCustom.triggered.connect( lambda: self._model.set_filetype("Custom")) openCustom.triggered.connect(lambda: self.customfiledialog.exec_()) openSignal.addAction(openCustom) segmentSignal = QAction("select segment", self) segmentSignal.triggered.connect(self.segmentermap.map) self.segmentermap.setMapping(segmentSignal, 1) signalmenu.addAction(segmentSignal) self.segmentermap.mapped.connect(self.toggle_segmenter) saveSignal = QAction("save", self) saveSignal.triggered.connect(self._controller.get_wpathsignal) signalmenu.addAction(saveSignal) # peak menu peakmenu = menubar.addMenu("peaks") findPeaks = QAction("find", self) findPeaks.triggered.connect(self._controller.find_peaks) peakmenu.addAction(findPeaks) autocorrectPeaks = QAction("autocorrect", self) autocorrectPeaks.triggered.connect(self._controller.autocorrect_peaks) peakmenu.addAction(autocorrectPeaks) savePeaks = QAction("save", self) savePeaks.triggered.connect(self._controller.get_wpathpeaks) peakmenu.addAction(savePeaks) loadPeaks = QAction("load", self) loadPeaks.triggered.connect(self._controller.get_rpathpeaks) peakmenu.addAction(loadPeaks) # stats menu statsmenu = menubar.addMenu("statistics") calculateStats = QAction("calculate", self) calculateStats.triggered.connect(self._controller.calculate_stats) statsmenu.addAction(calculateStats) saveStats = QAction("save", self) saveStats.triggered.connect(self._controller.get_wpathstats) statsmenu.addAction(saveStats) # set up status bar to display error messages and current file path self.statusBar = QStatusBar() self.setStatusBar(self.statusBar) self.progressBar = QProgressBar(self) self.progressBar.setRange(0, 1) self.statusBar.addPermanentWidget(self.progressBar) self.currentFile = QLabel() self.statusBar.addPermanentWidget(self.currentFile) # set up the central widget containing the plot and navigationtoolbar self.centwidget = QWidget() self.setCentralWidget(self.centwidget) # connect canvas0 to keyboard and mouse input for peak editing; # only widgets (e.g. canvas) that currently have focus capture # keyboard input: "You must enable keyboard focus for a widget if # it processes keyboard events." self.canvas0.setFocusPolicy(Qt.ClickFocus) self.canvas0.setFocus() self.canvas0.mpl_connect("key_press_event", self._controller.edit_peaks) self.canvas0.mpl_connect("button_press_event", self.get_xcursor) # arrange the three figure canvases in splitter object self.splitter = QSplitter(Qt.Vertical) # setting opaque resizing to false is important, since resizing gets # very slow otherwise once axes are populated self.splitter.setOpaqueResize(False) self.splitter.addWidget(self.canvas0) self.splitter.addWidget(self.canvas1) self.splitter.addWidget(self.canvas2) self.splitter.setChildrenCollapsible(False) # define GUI layout self.vlayout0 = QVBoxLayout(self.centwidget) self.vlayout1 = QVBoxLayout() self.vlayoutA = QFormLayout() self.vlayoutB = QFormLayout() self.vlayoutC = QVBoxLayout() self.vlayoutD = QVBoxLayout() self.hlayout0 = QHBoxLayout() self.optionsgroupA = QGroupBox("processing options") self.vlayoutA.addRow(self.modmenulabel, self.modmenu) self.vlayoutA.addRow(self.batchmenulabel, self.batchmenu) self.optionsgroupA.setLayout(self.vlayoutA) self.optionsgroupB = QGroupBox("channels") self.vlayoutB.addRow(self.sigchanmenulabel, self.sigchanmenu) self.vlayoutB.addRow(self.markerchanmenulabel, self.markerchanmenu) self.optionsgroupB.setLayout(self.vlayoutB) self.optionsgroupC = QGroupBox("peaks") self.vlayoutC.addWidget(self.editcheckbox) self.vlayoutC.addWidget(self.savecheckbox) self.vlayoutC.addWidget(self.correctcheckbox) self.optionsgroupC.setLayout(self.vlayoutC) self.optionsgroupD = QGroupBox("select statistics for saving") self.vlayoutD.addWidget(self.periodcheckbox) self.vlayoutD.addWidget(self.ratecheckbox) self.vlayoutD.addWidget(self.tidalampcheckbox) self.optionsgroupD.setLayout(self.vlayoutD) self.vlayout1.addWidget(self.optionsgroupA) self.vlayout1.addWidget(self.optionsgroupB) self.vlayout1.addWidget(self.optionsgroupC) self.vlayout1.addWidget(self.optionsgroupD) self.optionsgroupwidget = QWidget() self.optionsgroupwidget.setLayout(self.vlayout1) self.optionsgroup = QDockWidget("configurations", self) self.optionsgroup.setAllowedAreas(Qt.LeftDockWidgetArea) self.toggleoptionsgroup = self.optionsgroup.toggleViewAction() self.toggleoptionsgroup.setText("show/hide configurations") menubar.addAction(self.toggleoptionsgroup) self.optionsgroup.setWidget(self.optionsgroupwidget) self.addDockWidget(Qt.LeftDockWidgetArea, self.optionsgroup) self.vlayout0.addWidget(self.splitter) self.hlayout0.addWidget(self.navitools) self.vlayout0.addLayout(self.hlayout0) ############################################## # connect output widgets to external signals # ############################################## self._model.signal_changed.connect(self.plot_signal) self._model.marker_changed.connect(self.plot_marker) self._model.peaks_changed.connect(self.plot_peaks) self._model.period_changed.connect(self.plot_period) self._model.rate_changed.connect(self.plot_rate) self._model.tidalamp_changed.connect(self.plot_tidalamp) self._model.path_changed.connect(self.display_path) self._model.segment_changed.connect(self.plot_segment) self._model.status_changed.connect(self.display_status) self._model.progress_changed.connect(self.display_progress) self._model.model_reset.connect(self.reset_plot) ########### # methods # ########### def plot_signal(self, value): self.ax00.clear() self.ax00.relim() # reset navitools history self.navitools.update() self.line00 = self.ax00.plot(self._model.sec, value, zorder=1) self.ax00.set_xlabel("seconds", fontsize="large", fontweight="heavy") self.canvas0.draw() # print("plot_signal listening") # print(self.ax0.collections, self.ax0.patches, self.ax0.artists) def plot_peaks(self, value): # self.scat is listed in ax.collections if self.ax00.collections: self.ax00.collections[0].remove() self.scat = self.ax00.scatter(self._model.sec[value], self._model.signal[value], c="m", zorder=2) self.canvas0.draw() # print("plot_peaks listening") # print(self.ax0.collections, self.ax0.patches, self.ax0.artists) def plot_segment(self, value): # If an invalid signal has been selected reset the segmenter interface. if value is None: self.toggle_segmenter(1) return if self.ax00.patches: # self.segementspan is listed in ax.patches self.ax00.patches[0].remove() self.segmentspan = self.ax00.axvspan(value[0], value[1], color="m", alpha=0.25) self.canvas0.draw() self.confirmedit.setEnabled(True) # print(self.ax0.collections, self.ax0.patches, self.ax0.artists) def plot_marker(self, value): self.ax10.clear() self.ax10.relim() self.line10 = self.ax10.plot(value[0], value[1]) self.canvas1.draw() # print("plot_marker listening") def plot_period(self, value): self.ax20.clear() self.ax20.relim() self.navitools.home() if self._model.savestats["period"]: self.line20 = self.ax20.plot(self._model.sec, value, c="m") else: self.line20 = self.ax20.plot(self._model.sec, value) self.ax20.set_ylim(bottom=min(value), top=max(value)) self.ax20.set_title("period", pad=0, fontweight="heavy") self.ax20.grid(True, axis="y") self.navitools.update() self.canvas2.draw() # print("plot_period listening") def plot_rate(self, value): self.ax21.clear() self.ax21.relim() self.navitools.home() if self._model.savestats["rate"]: self.line21 = self.ax21.plot(self._model.sec, value, c="m") else: self.line21 = self.ax21.plot(self._model.sec, value) self.ax21.set_ylim(bottom=min(value), top=max(value)) self.ax21.set_title("rate", pad=0, fontweight="heavy") self.ax21.grid(True, axis="y") self.navitools.update() self.canvas2.draw() # print("plot_rate listening") def plot_tidalamp(self, value): self.ax22.clear() self.ax22.relim() self.navitools.home() if self._model.savestats["tidalamp"]: self.line22 = self.ax22.plot(self._model.sec, value, c="m") else: self.line22 = self.ax22.plot(self._model.sec, value) self.ax22.set_ylim(bottom=min(value), top=max(value)) self.ax22.set_title("amplitude", pad=0, fontweight="heavy") self.ax22.grid(True, axis="y") self.navitools.update() self.canvas2.draw() # print("plot_tidalamp listening") def display_path(self, value): self.currentFile.setText(value) def display_status(self, status): # display status until new status is set self.statusBar.showMessage(status) def display_progress(self, value): # if value is 0, the progressbar indicates a busy state self.progressBar.setRange(0, value) def toggle_segmenter(self, value): if not self._model.loaded: return # Open segmenter when called from signalmenu or clear segmenter # upon selection of invalid segment. if value == 1: self.segmenter.setVisible(True) self.confirmedit.setEnabled(False) self.startedit.clear() self.endedit.clear() if self.ax00.patches: self.ax00.patches[0].remove() self.canvas0.draw() # Close segmenter after segment has been confirmed. elif value == 0: self.segmenter.setVisible(False) if self.ax00.patches: self.ax00.patches[0].remove() self.canvas0.draw() # Close segmenter after segmentation has been aborted (reset # segment). elif value == 2: self._model.set_segment([0, 0]) # This will reset the model to None self.segmenter.setVisible(False) if self.ax00.patches: self.ax00.patches[0].remove() self.canvas0.draw() def enable_segmentedit(self): # disable peak editing to avoid interference self.editcheckbox.setChecked(False) if self.startedit.hasFocus(): self.segmentcursor = "start" elif self.endedit.hasFocus(): self.segmentcursor = "end" def set_customheader(self): """Populate the customheader with inputs from the customfiledialog""" # Check if one of the mandatory fields is missing. mandatoryfields = self.signaledit.text() and self.headerrowsedit.text( ) and self.sfreqedit.text() if not mandatoryfields: self._model.status = ( "Please provide values for 'biosignal column'" ", 'number of header rows' and 'sampling" " rate'.") return seps = {"comma": ",", "tab": "\t", "colon": ":", "space": " "} self._model.customheader = dict.fromkeys( self._model.customheader, None ) # reset header here since it cannot be reset in controller.get_fpaths() self._model.customheader["signalidx"] = int(self.signaledit.text()) self._model.customheader["skiprows"] = int(self.headerrowsedit.text()) self._model.customheader["sfreq"] = int(self.sfreqedit.text()) self._model.customheader["separator"] = seps[ self.separatormenu.currentText()] if self.markeredit.text(): # not mandatory self._model.customheader["markeridx"] = int(self.markeredit.text()) self.customfiledialog.done(QDialog.Accepted) # close the dialog window self._controller.get_fpaths() # move on to file selection def get_xcursor(self, event): # event.button 1 corresponds to left mouse button if event.button != 1: return # limit number of decimal places to two if self.segmentcursor == "start": self.startedit.selectAll() self.startedit.insert("{:.2f}".format(event.xdata)) elif self.segmentcursor == "end": self.endedit.selectAll() self.endedit.insert("{:.2f}".format(event.xdata)) # disable segment cursor again after value has been set self.segmentcursor = False def select_stats(self, event): """ select or deselect statistics to be saved; toggle boolean with xor operator ^=, toggle color with dictionary """ self._model.savestats[event] ^= True line = None if event == "period": if self.line20: line = self.line20[0] elif event == "rate": if self.line21: line = self.line21[0] elif event == "tidalamp": if self.line22: line = self.line22[0] if line: line.set_color(self.togglecolors[line.get_color()]) self.canvas2.draw() def toggle_options(self, event): if event in ["ECG", "PPG"]: self.tidalampcheckbox.setEnabled(False) self.tidalampcheckbox.setChecked(False) self.ax22.set_visible(False) self.canvas2.draw() elif event == "RESP": self.tidalampcheckbox.setEnabled(True) self.ax22.set_visible(True) self.canvas2.draw() elif event == "multiple files": self.editcheckbox.setEnabled(False) self.editcheckbox.setChecked(False) self.savecheckbox.setEnabled(True) self.correctcheckbox.setEnabled(True) self.markerchanmenu.setEnabled(False) elif event == "single file": self.editcheckbox.setEnabled(True) self.markerchanmenu.setEnabled(True) self.savecheckbox.setEnabled(False) self.savecheckbox.setChecked(False) self.correctcheckbox.setEnabled(False) self.correctcheckbox.setChecked(False) def reset_plot(self): self.ax00.clear() self.ax00.relim() self.line00 = None self.scat = None self.segmentspan = None self.ax10.clear() self.ax10.relim() self.line10 = None self.ax20.clear() self.ax20.relim() self.line20 = None self.ax21.clear() self.ax21.relim() self.line21 = None self.ax22.clear() self.ax22.relim() self.line22 = None self.canvas0.draw() self.canvas1.draw() self.canvas2.draw() self.navitools.update() self.currentFile.clear()
class MagnifierWidget(ToolWidget): def __init__(self, image, parent=None): super(MagnifierWidget, self).__init__(parent) self.equalize_radio = QRadioButton(self.tr("Equalization")) self.equalize_radio.setToolTip(self.tr("RGB histogram equalization")) self.contrast_radio = QRadioButton(self.tr("Auto Contrast")) self.contrast_radio.setToolTip(self.tr("Compress luminance tonality")) self.centile_spin = QSpinBox() self.centile_spin.setRange(0, 100) self.centile_spin.setValue(20) self.centile_spin.setSuffix(self.tr(" %")) self.centile_spin.setToolTip(self.tr("Histogram percentile amount")) self.channel_check = QCheckBox(self.tr("By channel")) self.channel_check.setToolTip(self.tr("Independent RGB compression")) self.equalize_radio.setChecked(True) self.last_radio = self.equalize_radio self.image = image self.viewer = ImageViewer(self.image, self.image) self.change() self.viewer.viewChanged.connect(self.process) self.equalize_radio.clicked.connect(self.change) self.contrast_radio.clicked.connect(self.change) self.centile_spin.valueChanged.connect(self.change) self.channel_check.stateChanged.connect(self.change) top_layout = QHBoxLayout() top_layout.addWidget(QLabel(self.tr("Mode:"))) top_layout.addWidget(self.equalize_radio) top_layout.addWidget(self.contrast_radio) top_layout.addWidget(self.centile_spin) top_layout.addWidget(self.channel_check) top_layout.addStretch() main_layout = QVBoxLayout() main_layout.addLayout(top_layout) main_layout.addWidget(self.viewer) self.setLayout(main_layout) def process(self, rect): y1 = rect.top() y2 = rect.bottom() x1 = rect.left() x2 = rect.right() roi = self.image[y1:y2, x1:x2] if self.equalize_radio.isChecked(): self.centile_spin.setEnabled(False) self.channel_check.setEnabled(False) roi = equalize_img(roi) self.last_radio = self.equalize_radio elif self.contrast_radio.isChecked(): self.centile_spin.setEnabled(True) self.channel_check.setEnabled(True) centile = self.centile_spin.value() / 200 if self.channel_check.isChecked(): roi = cv.merge( [cv.LUT(c, auto_lut(c, centile)) for c in cv.split(roi)]) else: roi = cv.LUT( roi, auto_lut(cv.cvtColor(roi, cv.COLOR_BGR2GRAY), centile)) self.last_radio = self.contrast_radio else: self.last_radio.setChecked(True) return processed = np.copy(self.image) processed[y1:y2, x1:x2] = roi self.viewer.update_processed(processed) def change(self): self.process(self.viewer.get_rect())
class PilotControls(QHBoxLayout): def __init__(self, roster: Optional[FlightRoster], idx: int) -> None: super().__init__() self.roster = roster self.pilot_index = idx self.selector = PilotSelector(roster, idx) self.selector.currentIndexChanged.connect(self.on_pilot_changed) self.addWidget(self.selector) self.player_checkbox = QCheckBox(text="Player") self.player_checkbox.setToolTip("Checked if this pilot is a player.") self.on_pilot_changed(self.selector.currentIndex()) self.addWidget(self.player_checkbox) self.player_checkbox.toggled.connect(self.on_player_toggled) @property def pilot(self) -> Optional[Pilot]: if self.roster is None or self.pilot_index >= self.roster.max_size: return None return self.roster.pilots[self.pilot_index] def on_player_toggled(self, checked: bool) -> None: pilot = self.pilot if pilot is None: logging.error( "Cannot toggle state of a pilot when none is selected") return pilot.player = checked def on_pilot_changed(self, index: int) -> None: pilot = self.selector.itemData(index) self.player_checkbox.blockSignals(True) try: self.player_checkbox.setChecked(pilot is not None and pilot.player) finally: self.player_checkbox.blockSignals(False) def update_available_pilots(self) -> None: self.selector.rebuild() def enable_and_reset(self) -> None: self.selector.rebuild() self.player_checkbox.setEnabled(True) self.on_pilot_changed(self.selector.currentIndex()) def disable_and_clear(self) -> None: self.selector.rebuild() self.player_checkbox.blockSignals(True) try: self.player_checkbox.setEnabled(False) self.player_checkbox.setChecked(False) finally: self.player_checkbox.blockSignals(False) def replace(self, new_roster: Optional[FlightRoster]) -> None: self.roster = new_roster if self.roster is None or self.pilot_index >= self.roster.max_size: self.disable_and_clear() else: self.enable_and_reset() self.selector.replace(new_roster)
class SubtitleInfoDialog(QDialog): def __init__(self, subtitle_name="Test", subtitle_delay=0.0, subtitle_language=Default_Subtitle_Language, subtitle_track_name="Test", subtitle_set_default=False, subtitle_set_forced=False, subtitle_default_value_delay=0.0, subtitle_default_value_language=Default_Subtitle_Language, subtitle_default_value_track_name="Test", subtitle_default_value_set_default=False, subtitle_default_value_set_forced=False, subtitle_set_default_disabled=False, subtitle_set_forced_disabled=False, disable_edit=False, parent=None): super().__init__(parent) self.window_title = "Subtitle Info" self.state = "no" self.messageIcon = QLabel() self.disable_edit = disable_edit self.current_subtitle_language = str(subtitle_language) self.current_subtitle_delay = str(subtitle_delay) self.current_subtitle_track_name = str(subtitle_track_name) self.current_subtitle_set_default = subtitle_set_default self.current_subtitle_set_forced = subtitle_set_forced self.default_subtitle_language = str(subtitle_default_value_language) self.default_subtitle_delay = str(subtitle_default_value_delay) self.default_subtitle_track_name = str( subtitle_default_value_track_name) self.default_subtitle_set_default = subtitle_default_value_set_default self.default_subtitle_set_forced = subtitle_default_value_set_forced self.subtitle_set_default_disabled = subtitle_set_default_disabled self.subtitle_set_forced_disabled = subtitle_set_forced_disabled self.subtitle_name_label = QLabel("Subtitle Name:") self.subtitle_name_value = QLabel(str(subtitle_name)) self.subtitle_delay_label = QLabel("Subtitle Delay:") self.subtitle_delay_spin = QDoubleSpinBox() self.setup_subtitle_delay_spin() self.subtitle_language_label = QLabel("Subtitle Language:") self.subtitle_language_comboBox = QComboBox() self.setup_subtitle_language_comboBox() self.subtitle_track_name_label = QLabel("Subtitle Track Name:") self.subtitle_track_name_lineEdit = QLineEdit() self.setup_subtitle_track_name_lineEdit() self.subtitle_set_forced_label = QLabel("Subtitle Forced State:") self.subtitle_set_forced_checkBox = QCheckBox() self.setup_subtitle_set_forced_checkBox() self.subtitle_set_default_label = QLabel("Subtitle Default State:") self.subtitle_set_default_checkBox = QCheckBox() self.setup_subtitle_set_default_checkBox() self.yes_button = QPushButton("OK") self.no_button = QPushButton("Cancel") self.reset_button = QPushButton("Reset To Default") self.buttons_layout = QHBoxLayout() self.subtitle_delay_layout = QHBoxLayout() self.subtitle_language_layout = QHBoxLayout() self.subtitle_track_name_layout = QHBoxLayout() self.subtitle_set_default_layout = QHBoxLayout() self.subtitle_set_forced_layout = QHBoxLayout() self.buttons_layout.addWidget(QLabel(""), stretch=3) self.buttons_layout.addWidget(self.reset_button, stretch=2) self.buttons_layout.addWidget(self.yes_button, stretch=2) self.buttons_layout.addWidget(self.no_button, stretch=2) self.buttons_layout.addWidget(QLabel(""), stretch=3) self.subtitle_setting_layout = QGridLayout() self.subtitle_changeble_setting_layout = QFormLayout() self.subtitle_changeble_setting_layout.addRow(self.subtitle_name_label, self.subtitle_name_value) self.subtitle_changeble_setting_layout.addRow( self.subtitle_track_name_label, self.subtitle_track_name_lineEdit) self.subtitle_changeble_setting_layout.addRow( self.subtitle_language_label, self.subtitle_language_comboBox) self.subtitle_changeble_setting_layout.addRow( self.subtitle_delay_label, self.subtitle_delay_spin) self.subtitle_changeble_setting_layout.addRow( self.subtitle_set_default_label, self.subtitle_set_default_checkBox) self.subtitle_changeble_setting_layout.addRow( self.subtitle_set_forced_label, self.subtitle_set_forced_checkBox) self.subtitle_setting_layout.addLayout( self.subtitle_changeble_setting_layout, 0, 0, 5, 2) self.subtitle_setting_layout.addWidget(self.messageIcon, 0, 3, 5, -1) self.main_layout = QGridLayout() self.main_layout.addLayout(self.subtitle_setting_layout, 0, 0, 2, 3) self.main_layout.addLayout(self.buttons_layout, 2, 0, 1, -1) self.main_layout.setContentsMargins(20, 20, 20, 20) self.setLayout(self.main_layout) self.setup_ui() self.signal_connect() def setup_ui(self): self.disable_question_mark_window() self.messageIcon.setPixmap( QtGui.QPixmap(GlobalFiles.SubtitleIconPath).scaledToHeight(100)) self.set_dialog_values() self.set_default_buttons() if self.subtitle_set_default_disabled: self.subtitle_set_default_disable() if self.subtitle_set_forced_disabled: self.subtitle_set_forced_disable() if self.disable_edit: self.subtitle_track_name_lineEdit.setEnabled(False) self.subtitle_language_comboBox.setEnabled(False) self.subtitle_delay_spin.setEnabled(False) self.subtitle_set_default_checkBox.setEnabled(False) self.subtitle_set_forced_checkBox.setEnabled(False) self.reset_button.setEnabled(False) self.setup_tool_tip_hint_subtitle_set_default() self.setup_tool_tip_hint_subtitle_set_forced() def signal_connect(self): self.subtitle_track_name_lineEdit.textEdited.connect( self.update_current_subtitle_track_name) self.subtitle_delay_spin.editingFinished.connect( self.update_current_subtitle_delay) self.subtitle_language_comboBox.currentTextChanged.connect( self.update_current_subtitle_language) self.subtitle_set_default_checkBox.stateChanged.connect( self.update_current_subtitle_set_default) self.subtitle_set_forced_checkBox.stateChanged.connect( self.update_current_subtitle_set_forced) self.yes_button.clicked.connect(self.click_yes) self.no_button.clicked.connect(self.click_no) self.reset_button.clicked.connect(self.reset_subtitle_setting) def click_yes(self): self.state = "yes" self.close() def click_no(self): self.close() def set_dialog_values(self): self.setWindowTitle(self.window_title) self.setWindowIcon(GlobalFiles.InfoSettingIcon) def disable_question_mark_window(self): self.setWindowFlag(Qt.WindowContextHelpButtonHint, on=False) def increase_message_font_size(self, value): message_font = self.message.font() message_font.setPointSize(self.message.fontInfo().pointSize() + value) self.message.setFont(message_font) def set_default_buttons(self): self.yes_button.setDefault(True) self.yes_button.setFocus() def showEvent(self, a0: QtGui.QShowEvent) -> None: super().showEvent(a0) self.setFixedSize(self.size()) def setup_subtitle_track_name_lineEdit(self): self.subtitle_track_name_lineEdit.setClearButtonEnabled(True) self.subtitle_track_name_lineEdit.setText( self.current_subtitle_track_name) def setup_subtitle_language_comboBox(self): self.subtitle_language_comboBox.addItems(AllSubtitlesLanguages) self.subtitle_language_comboBox.setCurrentIndex( AllSubtitlesLanguages.index(self.current_subtitle_language)) self.subtitle_language_comboBox.setMaxVisibleItems(8) self.subtitle_language_comboBox.setStyleSheet( "QComboBox { combobox-popup: 0; }") def setup_subtitle_delay_spin(self): # self.subtitle_delay_spin.setMaximumWidth(screen_size.width() // 16) self.subtitle_delay_spin.setDecimals(3) self.subtitle_delay_spin.setMinimum(-9999.0) self.subtitle_delay_spin.setMaximum(9999.0) self.subtitle_delay_spin.setSingleStep(0.5) self.subtitle_delay_spin.setValue(float(self.current_subtitle_delay)) def setup_subtitle_set_default_checkBox(self): self.subtitle_set_default_checkBox.setText("Set Default") self.subtitle_set_default_checkBox.setChecked( bool(self.current_subtitle_set_default)) def setup_subtitle_set_forced_checkBox(self): self.subtitle_set_forced_checkBox.setText("Set Forced") self.subtitle_set_forced_checkBox.setChecked( bool(self.current_subtitle_set_forced)) def update_current_subtitle_track_name(self): self.current_subtitle_track_name = str( self.subtitle_track_name_lineEdit.text()) def update_current_subtitle_delay(self): self.current_subtitle_delay = round(self.subtitle_delay_spin.value(), 5) def update_current_subtitle_language(self): self.current_subtitle_language = str( self.subtitle_language_comboBox.currentText()) def update_current_subtitle_set_default(self): self.current_subtitle_set_default = ( self.subtitle_set_default_checkBox.checkState() == Qt.Checked) def update_current_subtitle_set_forced(self): self.current_subtitle_set_forced = ( self.subtitle_set_forced_checkBox.checkState() == Qt.Checked) def reset_subtitle_setting(self): self.current_subtitle_language = self.default_subtitle_language self.current_subtitle_delay = self.default_subtitle_delay self.current_subtitle_track_name = self.default_subtitle_track_name self.current_subtitle_set_default = self.default_subtitle_set_default self.current_subtitle_set_forced = self.default_subtitle_set_forced self.subtitle_language_comboBox.setCurrentIndex( AllSubtitlesLanguages.index(self.current_subtitle_language)) self.subtitle_delay_spin.setValue(float(self.current_subtitle_delay)) self.subtitle_track_name_lineEdit.setText( self.current_subtitle_track_name) self.subtitle_set_default_checkBox.setChecked( bool(self.current_subtitle_set_default)) self.subtitle_set_forced_checkBox.setChecked( bool(self.current_subtitle_set_forced)) def subtitle_set_default_disable(self): self.subtitle_set_default_checkBox.setDisabled(True) def subtitle_set_forced_disable(self): self.subtitle_set_forced_checkBox.setDisabled(True) def setup_tool_tip_hint_subtitle_set_default(self): if self.subtitle_set_default_checkBox.isEnabled(): self.subtitle_set_default_checkBox.setToolTip( "<nobr>set this subtitle to be the default subtitle track " "when play") self.subtitle_set_default_checkBox.setToolTipDuration(12000) else: self.subtitle_set_default_checkBox.setToolTip( "<nobr>set this subtitle to be the default subtitle track when play<br><b>Disabled</b> because " "option " "<b>make this subtitle default</b> is enabled on mux setting tab " ) self.subtitle_set_default_checkBox.setToolTipDuration(12000) def setup_tool_tip_hint_subtitle_set_forced(self): if self.subtitle_set_forced_checkBox.isEnabled(): self.subtitle_set_forced_checkBox.setToolTip( "<nobr>set this subtitle to be the forced subtitle track when " "play") self.subtitle_set_forced_checkBox.setToolTipDuration(12000) else: self.subtitle_set_forced_checkBox.setToolTip( "<nobr>set this subtitle to be the forced subtitle track when play<br><b>Disabled</b> because " "option " "<b>make this subtitle default and forced</b> is enabled on mux setting tab " ) self.subtitle_set_forced_checkBox.setToolTipDuration(12000) def execute(self): self.exec_()
class MainWindow(QWidget): def __init__(self, appctx): super().__init__() self.appctx = appctx self.api = None self.settings = Settings() self.gateway = Gateway() self.timers: Dict[str, QTimer] = {} self.init_ui() def init_ui(self): self.vbox = QVBoxLayout() self.hbox = QHBoxLayout() # Set layout self.setLayout(self.vbox) # Group list vbox3 = QVBoxLayout() group_frame = QGroupBox("Device Groups") group_frame.setLayout(QVBoxLayout()) self.group_list = QListWidget() self.group_list.itemPressed.connect(self.group_selected) group_frame.layout().addWidget(self.group_list) vbox3.addWidget(group_frame) # Sliders self.group_toggle = QCheckBox("Power") self.group_toggle.setEnabled(False) vbox3.addWidget(self.group_toggle) vbox3.addWidget(QLabel("Brightness")) self.group_brightness_slider = QSlider(Qt.Orientation.Horizontal) self.group_brightness_slider.setEnabled(False) self.group_brightness_slider.sliderMoved.connect( self.group_brightness_changed) vbox3.addWidget(self.group_brightness_slider) vbox3.addWidget(QLabel("Color Temperature")) self.group_color_slider = QSlider(Qt.Orientation.Horizontal) self.group_color_slider.setEnabled(False) self.group_color_slider.sliderMoved.connect(self.group_color_changed) vbox3.addWidget(self.group_color_slider) self.hbox.addLayout(vbox3) # moods mood_frame = QGroupBox("Moods") mood_frame.setLayout(QVBoxLayout()) self.mood_list = QListWidget() self.mood_list.itemPressed.connect(self.mood_selected) mood_frame.layout().addWidget(self.mood_list) self.hbox.addWidget(mood_frame) # Devices in group vbox2 = QVBoxLayout() device_frame = QGroupBox("Devices in Group") device_frame.setLayout(QVBoxLayout()) self.device_list = QListWidget() self.device_list.setEnabled(False) self.device_list.itemPressed.connect(self.device_selected) device_frame.layout().addWidget(self.device_list) vbox2.addWidget(device_frame) # Sliders self.device_toggle = QCheckBox("Power") self.device_toggle.setEnabled(False) vbox2.addWidget(self.device_toggle) vbox2.addWidget(QLabel("Brightness")) self.brightness_slider = QSlider(Qt.Orientation.Horizontal) self.brightness_slider.setEnabled(False) self.brightness_slider.sliderMoved.connect(self.brightness_changed) vbox2.addWidget(self.brightness_slider) vbox2.addWidget(QLabel("Color Temperature")) self.color_slider = QSlider(Qt.Orientation.Horizontal) self.color_slider.setEnabled(False) self.color_slider.sliderMoved.connect(self.color_changed) vbox2.addWidget(self.color_slider) self.hbox.addLayout(vbox2) self.vbox.addLayout(self.hbox) # Settings button icon = QIcon(resource_path('icons/settings.png')) self.settings_button = QPushButton(icon, "Settings") self.settings_button.pressed.connect(self.settings_pressed) self.vbox.addWidget(self.settings_button) self.setWindowTitle('TradfriGUI') self.re_init() def re_init(self): if self.settings.gateway_ip is None or self.settings.gateway_ip == '': self.settings_pressed() self.api = get_api(self.settings) self.device_list.clear() self.group_list.clear() if self.api is None: return groups = self.api(self.gateway.get_groups()) if len(groups) == 0: self.group_list.setEnabled(False) # TODO: load devices directly for group in groups: item = self.api(group) list_item = QListWidgetItem(item.name, self.group_list) setattr(list_item, 'api_item', item) def group_selected(self): current_item = self.group_list.currentItem() item = getattr(current_item, 'api_item', None) if item is None: return # refresh from gateway item = self.api(self.gateway.get_group(item.id)) # load moods self.mood_list.clear() moods = self.api(item.moods()) for m in moods: mood = self.api(m) list_item = QListWidgetItem(mood.name, self.mood_list) setattr(list_item, 'api_item', mood) # load devices devices = item.members() self.device_list.clear() # determine shared state and add devices to list state = False color_temp = False min_color = 10000 max_color = 0 color = [] brightness = [] for d in devices: device = self.api(d) if device.has_light_control: if device.light_control.lights[0].state: state = True if device.light_control.can_set_dimmer: if device.light_control.lights[0].state: brightness.append( device.light_control.lights[0].dimmer) else: brightness.append(0) if device.light_control.can_set_temp: color_temp = True min_color = min(min_color, device.light_control.min_mireds) max_color = max(max_color, device.light_control.max_mireds) color.append(device.light_control.lights[0].color_temp) list_item = QListWidgetItem(device.name, self.device_list) setattr(list_item, 'api_item', device) if len(brightness) > 0: brightness = int(sum(brightness) / len(brightness)) else: brightness = 0 if len(color) > 0: color = int(sum(color) / len(color)) else: color = min_color # enable device list and controls self.device_list.setEnabled(True) self.group_brightness_slider.setEnabled(True) self.group_brightness_slider.setMinimum(0) self.group_brightness_slider.setMaximum(254) self.group_brightness_slider.setSingleStep(16) self.group_brightness_slider.setValue(brightness) if color_temp: self.group_color_slider.setEnabled(True) self.group_color_slider.setMinimum(min_color) self.group_color_slider.setMaximum(max_color) self.group_color_slider.setSingleStep( int((max_color - min_color) / 10)) self.group_color_slider.setValue(color) else: self.group_color_slider.setEnabled(False) self.group_toggle.setEnabled(True) try: self.group_toggle.stateChanged.disconnect(self.group_toggled) except RuntimeError: pass # Disconnect failed because nothing was connected self.group_toggle.setCheckState( Qt.CheckState.Checked if state else Qt.CheckState.Unchecked) self.group_toggle.stateChanged.connect(self.group_toggled) self.brightness_slider.setEnabled(False) self.color_slider.setEnabled(False) self.device_toggle.setEnabled(False) def device_selected(self): current_item = self.device_list.currentItem() item = getattr(current_item, 'api_item', None) if item is None: return # refresh from gateway item = self.api(self.gateway.get_device(item.id)) # enable appropriate controls if item.has_light_control: ctrl = item.light_control if ctrl.can_set_dimmer: self.brightness_slider.setEnabled(True) self.brightness_slider.setMinimum(0) self.brightness_slider.setMaximum(254) self.brightness_slider.setSingleStep(16) self.brightness_slider.setValue(ctrl.lights[0].dimmer) else: self.brightness_slider.setEnabled(False) if ctrl.can_set_temp: self.color_slider.setEnabled(True) self.color_slider.setMinimum(ctrl.min_mireds) self.color_slider.setMaximum(ctrl.max_mireds) self.color_slider.setSingleStep( int((ctrl.max_mireds - ctrl.min_mireds) / 10)) self.color_slider.setValue(ctrl.lights[0].color_temp) else: self.color_slider.setEnabled(False) self.device_toggle.setEnabled(True) try: self.device_toggle.stateChanged.disconnect(self.device_toggled) except RuntimeError: pass # disconnect failed because nothing was connected self.device_toggle.setCheckState( Qt.CheckState.Checked if ctrl.lights[0].state else Qt. CheckState.Unchecked) self.device_toggle.stateChanged.connect(self.device_toggled) else: self.brightness_slider.setEnabled(False) self.color_slider.setEnabled(False) self.device_toggle.setEnabled(False) def mood_selected(self): current_group = self.group_list.currentItem() group = getattr(current_group, 'api_item', None) if group is None: return # refresh from gateway group = self.api(self.gateway.get_group(group.id)) current_mood = self.mood_list.currentItem() mood = getattr(current_mood, 'api_item', None) if mood is None: return self.api(group.activate_mood(mood.id)) def group_brightness_changed(self): current_item = self.group_list.currentItem() if current_item is None: return item = getattr(current_item, 'api_item', None) command = item.set_dimmer(self.group_brightness_slider.value(), transition_time=2) self.queue_command('group_brightness', command) def group_color_changed(self): current_item = self.group_list.currentItem() if current_item is None: return item = getattr(current_item, 'api_item', None) command = item.set_color_temp(self.group_color_slider.value(), transition_time=2) self.queue_command('group_color', command) def brightness_changed(self): current_item = self.device_list.currentItem() if current_item is None: return item = getattr(current_item, 'api_item', None) command = item.light_control.set_dimmer(self.brightness_slider.value(), transition_time=2) self.queue_command('device_brightness_{}'.format(item.id), command) def color_changed(self): current_item = self.device_list.currentItem() if current_item is None: return item = getattr(current_item, 'api_item', None) command = item.light_control.set_color_temp(self.color_slider.value(), transition_time=2) self.queue_command('device_color_{}'.format(item.id), command) def device_toggled(self): current_item = self.device_list.currentItem() if current_item is None: return item = getattr(current_item, 'api_item', None) command = item.light_control.set_state( self.device_toggle.checkState() == Qt.CheckState.Checked) self.api(command) def group_toggled(self): current_item = self.group_list.currentItem() if current_item is None: return item = getattr(current_item, 'api_item', None) command = item.set_state( self.group_toggle.checkState() == Qt.CheckState.Checked) self.api(command) def settings_pressed(self): config = ConfigWindow(self.appctx, self) config.setWindowModality(Qt.ApplicationModal) config.exec_() # reload settings self.settings = config.settings # re-initialize window self.re_init() def queue_command(self, name, command): timer = self.timers.get(name, None) if timer is None: timer = QTimer() timer.setInterval(200) timer.setSingleShot(True) timer.timeout.connect(self.timeout) timer.start() setattr(timer, 'command', command) self.timers[name] = timer def timeout(self): remove = [] for key, item in self.timers.items(): if item.isActive() == False: cmd = getattr(item, 'command') self.api(cmd) remove.append(key) for key in remove: del self.timers[key]