コード例 #1
0
class SimpleMeasurements(QWidget):
    def __init__(self, settings: StackSettings, parent=None):
        super().__init__(parent)
        self.settings = settings
        self.calculate_btn = QPushButton("Calculate")
        self.calculate_btn.clicked.connect(self.calculate)
        self.result_view = QTableWidget()
        self.channel_select = ChannelComboBox()
        self.units_select = EnumComboBox(Units)
        self.units_select.set_value(self.settings.get("simple_measurements.units", Units.nm))
        self.units_select.currentIndexChanged.connect(self.change_units)

        layout = QHBoxLayout()
        self.measurement_layout = QVBoxLayout()
        l1 = QHBoxLayout()
        l1.addWidget(QLabel("Units"))
        l1.addWidget(self.units_select)
        self.measurement_layout.addLayout(l1)
        l2 = QHBoxLayout()
        l2.addWidget(QLabel("Channel"))
        l2.addWidget(self.channel_select)
        self.measurement_layout.addLayout(l2)
        layout.addLayout(self.measurement_layout)
        result_layout = QVBoxLayout()
        result_layout.addWidget(self.result_view)
        result_layout.addWidget(self.calculate_btn)
        layout.addLayout(result_layout)
        self.setLayout(layout)
        self.setWindowTitle("Measurement")
        if self.window() == self:
            try:
                geometry = self.settings.get_from_profile("simple_measurement_window_geometry")
                self.restoreGeometry(QByteArray.fromHex(bytes(geometry, "ascii")))
            except KeyError:
                pass

    def closeEvent(self, event: QCloseEvent) -> None:
        """
        Save geometry if widget is used as standalone window.
        """
        if self.window() == self:
            self.settings.set_in_profile(
                "simple_measurement_window_geometry", self.saveGeometry().toHex().data().decode("ascii")
            )
        super().closeEvent(event)

    def calculate(self):
        if self.settings.segmentation is None:
            QMessageBox.warning(self, "No segmentation", "need segmentation to work")
            return
        to_calculate = []
        for i in range(2, self.measurement_layout.count()):
            # noinspection PyTypeChecker
            chk: QCheckBox = self.measurement_layout.itemAt(i).widget()
            if chk.isChecked():
                leaf: Leaf = MEASUREMENT_DICT[chk.text()].get_starting_leaf()
                to_calculate.append(leaf.replace_(per_component=PerComponent.Yes, area=AreaType.ROI))
        if not to_calculate:
            QMessageBox.warning(self, "No measurement", "Select at least one measurement")
            return

        profile = MeasurementProfile("", [MeasurementEntry(x.name, x) for x in to_calculate])

        dial = ExecuteFunctionDialog(
            profile.calculate,
            kwargs={
                "channel": self.settings.image.get_channel(self.channel_select.get_value()),
                "segmentation": self.settings.segmentation,
                "mask": None,
                "voxel_size": self.settings.image.spacing,
                "result_units": self.units_select.get_value(),
            },
        )
        dial.exec()
        result: MeasurementResult = dial.get_result()
        values = result.get_separated()
        labels = result.get_labels()
        self.result_view.clear()
        self.result_view.setColumnCount(len(values) + 1)
        self.result_view.setRowCount(len(labels))
        for i, val in enumerate(labels):
            self.result_view.setItem(i, 0, QTableWidgetItem(val))
        for j, values_list in enumerate(values):
            for i, val in enumerate(values_list):
                self.result_view.setItem(i, j + 1, QTableWidgetItem(str(val)))

    def _clean_measurements(self):
        selected = set()
        for _ in range(self.measurement_layout.count() - 2):
            # noinspection PyTypeChecker
            chk: QCheckBox = self.measurement_layout.takeAt(2).widget()
            if chk.isChecked():
                selected.add(chk.text())
            chk.deleteLater()
        return selected

    def event(self, event: QEvent) -> bool:
        if event.type() == QEvent.WindowActivate:
            self.channel_select.change_channels_num(self.settings.image.channels)
            selected = self._clean_measurements()
            for val in MEASUREMENT_DICT.values():
                area = val.get_starting_leaf().area
                pc = val.get_starting_leaf().per_component
                if (
                    val.get_fields()
                    or (area is not None and area != AreaType.ROI)
                    or (pc is not None and pc != PerComponent.Yes)
                ):
                    continue
                text = val.get_name()
                chk = QCheckBox(text)
                if text in selected:
                    chk.setChecked(True)
                self.measurement_layout.addWidget(chk)
        return super().event(event)

    def change_units(self):
        self.settings.set("simple_measurements.units", self.units_select.get_value())
コード例 #2
0
class SimpleMeasurements(QWidget):
    def __init__(self, settings: StackSettings, parent=None):
        super().__init__(parent)
        self.settings = settings
        self.calculate_btn = QPushButton("Calculate")
        self.calculate_btn.clicked.connect(self.calculate)
        self.result_view = QTableWidget()
        self.channel_select = ChannelComboBox()
        self.units_select = QEnumComboBox(enum_class=Units)
        self.units_select.setCurrentEnum(
            self.settings.get("simple_measurements.units", Units.nm))
        self.units_select.currentIndexChanged.connect(self.change_units)
        self._shift = 2

        layout = QHBoxLayout()
        self.measurement_layout = QVBoxLayout()
        l1 = QHBoxLayout()
        l1.addWidget(QLabel("Units"))
        l1.addWidget(self.units_select)
        self.measurement_layout.addLayout(l1)
        l2 = QHBoxLayout()
        l2.addWidget(QLabel("Channel"))
        l2.addWidget(self.channel_select)
        self.measurement_layout.addLayout(l2)
        layout.addLayout(self.measurement_layout)
        result_layout = QVBoxLayout()
        result_layout.addWidget(self.result_view)
        result_layout.addWidget(self.calculate_btn)
        layout.addLayout(result_layout)
        self.setLayout(layout)
        self.setWindowTitle("Measurement")
        if self.window() == self:
            with suppress(KeyError):
                geometry = self.settings.get_from_profile(
                    "simple_measurement_window_geometry")
                self.restoreGeometry(
                    QByteArray.fromHex(bytes(geometry, "ascii")))

    def closeEvent(self, event: QCloseEvent) -> None:
        """
        Save geometry if widget is used as standalone window.
        """
        if self.window() == self:
            self.settings.set_in_profile(
                "simple_measurement_window_geometry",
                self.saveGeometry().toHex().data().decode("ascii"))
        super().closeEvent(event)

    def calculate(self):
        if self.settings.roi is None:
            QMessageBox.warning(self, "No segmentation",
                                "need segmentation to work")
            return
        to_calculate = []
        for i in range(self._shift, self.measurement_layout.count()):
            # noinspection PyTypeChecker
            chk: QCheckBox = self.measurement_layout.itemAt(i).widget()
            if chk.isChecked():
                leaf: Leaf = MEASUREMENT_DICT[chk.text()].get_starting_leaf()
                to_calculate.append(
                    leaf.replace_(per_component=PerComponent.Yes,
                                  area=AreaType.ROI))
        if not to_calculate:
            QMessageBox.warning(self, "No measurement",
                                "Select at least one measurement")
            return

        profile = MeasurementProfile(
            "", [MeasurementEntry(x.name, x) for x in to_calculate])

        dial = ExecuteFunctionDialog(
            profile.calculate,
            kwargs={
                "image": self.settings.image,
                "channel_num": self.channel_select.get_value(),
                "roi": self.settings.roi_info,
                "result_units": self.units_select.currentEnum(),
            },
        )
        dial.exec_()
        result: MeasurementResult = dial.get_result()
        values = result.get_separated()
        labels = result.get_labels()
        self.result_view.clear()
        self.result_view.setColumnCount(len(values) + 1)
        self.result_view.setRowCount(len(labels))
        for i, val in enumerate(labels):
            self.result_view.setItem(i, 0, QTableWidgetItem(val))
        for j, values_list in enumerate(values):
            for i, val in enumerate(values_list):
                self.result_view.setItem(i, j + 1, QTableWidgetItem(str(val)))

    def _clean_measurements(self):
        selected = set()
        for _ in range(self.measurement_layout.count() - self._shift):
            # noinspection PyTypeChecker
            chk: QCheckBox = self.measurement_layout.takeAt(
                self._shift).widget()
            if chk.isChecked():
                selected.add(chk.text())
            chk.deleteLater()
        return selected

    def refresh_measurements(self):
        selected = self._clean_measurements()
        for val in MEASUREMENT_DICT.values():
            area = val.get_starting_leaf().area
            pc = val.get_starting_leaf().per_component
            if (val.get_fields() or (area is not None and area != AreaType.ROI)
                    or (pc is not None and pc != PerComponent.Yes)):
                continue
            text = val.get_name()
            chk = QCheckBox(text)
            if text in selected:
                chk.setChecked(True)
            self.measurement_layout.addWidget(chk)

    def keyPressEvent(self, e: QKeyEvent):
        if not e.modifiers() & Qt.ControlModifier:
            return
        selected = self.result_view.selectedRanges()

        if e.key() == Qt.Key_C:  # copy
            s = ""

            for r in range(selected[0].topRow(), selected[0].bottomRow() + 1):
                for c in range(selected[0].leftColumn(),
                               selected[0].rightColumn() + 1):
                    try:
                        s += str(self.result_view.item(r, c).text()) + "\t"
                    except AttributeError:
                        s += "\t"
                s = s[:-1] + "\n"  # eliminate last '\t'
            QApplication.clipboard().setText(s)

    def event(self, event: QEvent) -> bool:
        if event.type() == QEvent.WindowActivate:
            if self.settings.image is not None:
                self.channel_select.change_channels_num(
                    self.settings.image.channels)
            self.refresh_measurements()

        return super().event(event)

    def change_units(self):
        self.settings.set("simple_measurements.units",
                          self.units_select.currentEnum())
コード例 #3
0
class MeasurementWidget(QWidget):
    """
    :type settings: Settings
    :type segment: Segment
    """
    def __init__(self, settings: PartSettings, segment=None):
        super().__init__()
        self.settings = settings
        self.segment = segment
        self.measurements_storage = MeasurementsStorage()
        self.recalculate_button = QPushButton(
            "Recalculate and\n replace measurement", self)
        self.recalculate_button.clicked.connect(
            self.replace_measurement_result)
        self.recalculate_append_button = QPushButton(
            "Recalculate and\n append measurement", self)
        self.recalculate_append_button.clicked.connect(
            self.append_measurement_result)
        self.copy_button = QPushButton("Copy to clipboard", self)
        self.copy_button.setToolTip(
            "You cacn copy also with 'Ctrl+C'. To get raw copy copy with 'Ctrl+Shit+C'"
        )
        self.horizontal_measurement_present = QCheckBox(
            "Horizontal view", self)
        self.no_header = QCheckBox("No header", self)
        self.no_units = QCheckBox("No units", self)
        self.no_units.setChecked(True)
        self.no_units.clicked.connect(self.refresh_view)
        self.expand_mode = QCheckBox("Expand", self)
        self.expand_mode.setToolTip(
            "Shows results for each component in separate entry")
        self.file_names = EnumComboBox(FileNamesEnum)
        self.file_names_label = QLabel("Add file name:")
        self.file_names.currentIndexChanged.connect(self.refresh_view)
        self.horizontal_measurement_present.stateChanged.connect(
            self.refresh_view)
        self.expand_mode.stateChanged.connect(self.refresh_view)
        self.copy_button.clicked.connect(self.copy_to_clipboard)
        self.measurement_type = SearchCombBox(self)
        # noinspection PyUnresolvedReferences
        self.measurement_type.currentIndexChanged.connect(
            self.measurement_profile_selection_changed)
        self.measurement_type.addItem("<none>")
        self.measurement_type.addItems(
            list(sorted(self.settings.measurement_profiles.keys())))
        self.measurement_type.setToolTip(
            'You can create new measurement profile in advanced window, in tab "Measurement settings"'
        )
        self.channels_chose = ChannelComboBox()
        self.units_choose = EnumComboBox(Units)
        self.units_choose.set_value(self.settings.get("units_value", Units.nm))
        self.info_field = QTableWidget(self)
        self.info_field.setColumnCount(3)
        self.info_field.setHorizontalHeaderLabels(["Name", "Value", "Units"])
        self.measurement_add_shift = 0
        layout = QVBoxLayout()
        # layout.addWidget(self.recalculate_button)
        v_butt_layout = QVBoxLayout()
        v_butt_layout.setSpacing(1)
        self.up_butt_layout = QHBoxLayout()
        self.up_butt_layout.addWidget(self.recalculate_button)
        self.up_butt_layout.addWidget(self.recalculate_append_button)
        self.butt_layout = QHBoxLayout()
        # self.butt_layout.setMargin(0)
        # self.butt_layout.setSpacing(10)
        self.butt_layout.addWidget(self.horizontal_measurement_present, 1)
        self.butt_layout.addWidget(self.no_header, 1)
        self.butt_layout.addWidget(self.no_units, 1)
        self.butt_layout.addWidget(self.expand_mode, 1)
        self.butt_layout.addWidget(self.file_names_label)
        self.butt_layout.addWidget(self.file_names, 1)
        self.butt_layout.addWidget(self.copy_button, 2)
        self.butt_layout2 = QHBoxLayout()
        self.butt_layout3 = QHBoxLayout()
        self.butt_layout3.addWidget(QLabel("Channel:"))
        self.butt_layout3.addWidget(self.channels_chose)
        self.butt_layout3.addWidget(QLabel("Units:"))
        self.butt_layout3.addWidget(self.units_choose)
        # self.butt_layout3.addWidget(QLabel("Noise removal:"))
        # self.butt_layout3.addWidget(self.noise_removal_method)
        self.butt_layout3.addWidget(QLabel("Measurement set:"))
        self.butt_layout3.addWidget(self.measurement_type, 2)
        v_butt_layout.addLayout(self.up_butt_layout)
        v_butt_layout.addLayout(self.butt_layout)
        v_butt_layout.addLayout(self.butt_layout2)
        v_butt_layout.addLayout(self.butt_layout3)
        layout.addLayout(v_butt_layout)
        # layout.addLayout(self.butt_layout)
        layout.addWidget(self.info_field)
        self.setLayout(layout)
        # noinspection PyArgumentList
        self.clip = QApplication.clipboard()
        self.settings.image_changed[int].connect(self.image_changed)
        self.previous_profile = None

    def check_if_measurement_can_be_calculated(self, name):
        if name == "<none>":
            return "<none>"
        profile: MeasurementProfile = self.settings.measurement_profiles.get(
            name)
        if profile.is_any_mask_measurement() and self.settings.mask is None:
            QMessageBox.information(
                self, "Need mask",
                "To use this measurement set please use data with mask loaded",
                QMessageBox.Ok)
            self.measurement_type.setCurrentIndex(0)
            return "<none>"
        if self.settings.roi is None:
            QMessageBox.information(
                self,
                "Need segmentation",
                'Before calculating please create segmentation ("Execute" button)',
                QMessageBox.Ok,
            )
            self.measurement_type.setCurrentIndex(0)
            return "<none>"
        return name

    def image_changed(self, channels_num):
        self.channels_chose.change_channels_num(channels_num)

    def measurement_profile_selection_changed(self, index):
        text = self.measurement_type.itemText(index)
        text = self.check_if_measurement_can_be_calculated(text)
        try:
            stat = self.settings.measurement_profiles[text]
            is_mask = stat.is_any_mask_measurement()
            disable = is_mask and (self.settings.mask is None)
        except KeyError:
            disable = True
        self.recalculate_button.setDisabled(disable)
        self.recalculate_append_button.setDisabled(disable)
        if disable:
            self.recalculate_button.setToolTip(
                "Measurement profile contains mask measurements when mask is not loaded"
            )
            self.recalculate_append_button.setToolTip(
                "Measurement profile contains mask measurements when mask is not loaded"
            )
        else:
            self.recalculate_button.setToolTip("")
            self.recalculate_append_button.setToolTip("")

    def copy_to_clipboard(self):
        s = ""
        for r in range(self.info_field.rowCount()):
            for c in range(self.info_field.columnCount()):
                try:
                    s += str(self.info_field.item(r, c).text()) + "\t"
                except AttributeError:
                    s += "\t"
            s = s[:-1] + "\n"  # eliminate last '\t'
        self.clip.setText(s)

    def replace_measurement_result(self):
        self.measurements_storage.clear()
        self.previous_profile = ""
        self.append_measurement_result()

    def refresh_view(self):
        self.measurements_storage.set_expand(self.expand_mode.isChecked())
        self.measurements_storage.set_show_units(not self.no_units.isChecked())
        self.info_field.clear()
        save_orientation = self.horizontal_measurement_present.isChecked()
        columns, rows = self.measurements_storage.get_size(save_orientation)
        self.info_field.setColumnCount(columns)
        self.info_field.setRowCount(rows)
        self.info_field.setHorizontalHeaderLabels(
            self.measurements_storage.get_header(save_orientation))
        self.info_field.setVerticalHeaderLabels(
            self.measurements_storage.get_rows(save_orientation))
        for x in range(rows):
            for y in range(columns):
                self.info_field.setItem(
                    x, y,
                    QTableWidgetItem(
                        self.measurements_storage.get_val_as_str(
                            x, y, save_orientation)))
        if self.file_names.get_value() == FileNamesEnum.No:
            if save_orientation:
                self.info_field.removeColumn(0)
            else:
                self.info_field.removeRow(0)
        elif self.file_names.get_value() == FileNamesEnum.Short:
            if save_orientation:
                columns = 1
            else:
                rows = 1
            for x in range(rows):
                for y in range(columns):
                    item = self.info_field.item(x, y)
                    item.setText(os.path.basename(item.text()))

        self.info_field.setEditTriggers(QAbstractItemView.NoEditTriggers)

    def append_measurement_result(self):
        try:
            compute_class = self.settings.measurement_profiles[
                self.measurement_type.currentText()]
        except KeyError:
            QMessageBox.warning(
                self,
                "Measurement profile not found",
                f"Measurement profile '{self.measurement_type.currentText()}' not found'",
            )
            return

        if self.settings.roi is None:
            return
        units = self.units_choose.get_value()

        # FIXME find which errors should be displayed as warning
        # def exception_hook(exception):
        #    QMessageBox.warning(self, "Calculation error", f"Error during calculation: {exception}")

        for num in compute_class.get_channels_num():
            if num >= self.settings.image.channels:
                QMessageBox.warning(
                    self,
                    "Measurement error",
                    "Cannot calculate this measurement because "
                    f"image do not have channel {num+1}",
                )
                return

        thread = ExecuteFunctionThread(
            compute_class.calculate,
            [
                self.settings.image,
                self.channels_chose.currentIndex(), self.settings.roi_info,
                units
            ],
        )
        dial = WaitingDialog(
            thread,
            "Measurement calculation")  # , exception_hook=exception_hook)
        dial.exec()
        stat: MeasurementResult = thread.result
        if stat is None:
            return
        stat.set_filename(self.settings.image_path)
        self.measurements_storage.add_measurements(stat)
        self.previous_profile = compute_class.name
        self.refresh_view()

    def keyPressEvent(self, e: QKeyEvent):
        if e.modifiers() & Qt.ControlModifier:
            selected = self.info_field.selectedRanges()

            if e.key() == Qt.Key_C:  # copy
                s = ""

                for r in range(selected[0].topRow(),
                               selected[0].bottomRow() + 1):
                    for c in range(selected[0].leftColumn(),
                                   selected[0].rightColumn() + 1):
                        try:
                            s += str(self.info_field.item(r, c).text()) + "\t"
                        except AttributeError:
                            s += "\t"
                    s = s[:-1] + "\n"  # eliminate last '\t'
                self.clip.setText(s)

    def update_measurement_list(self):
        self.measurement_type.blockSignals(True)
        available = list(sorted(self.settings.measurement_profiles.keys()))
        text = self.measurement_type.currentText()
        try:
            index = available.index(text) + 1
        except ValueError:
            index = 0
        self.measurement_type.clear()
        self.measurement_type.addItem("<none>")
        self.measurement_type.addItems(available)
        self.measurement_type.setCurrentIndex(index)
        self.measurement_type.blockSignals(False)

    def showEvent(self, _):
        self.update_measurement_list()

    def event(self, event: QEvent):
        if event.type() == QEvent.WindowActivate:
            self.update_measurement_list()
        return super().event(event)

    @staticmethod
    def _move_widgets(widgets_list: List[Tuple[QWidget, int]],
                      layout1: QBoxLayout, layout2: QBoxLayout):
        for el in widgets_list:
            layout1.removeWidget(el[0])
            layout2.addWidget(el[0], el[1])

    def resizeEvent(self, _event: QResizeEvent) -> None:
        if self.width() < 800 and self.butt_layout2.count() == 0:
            self._move_widgets(
                [(self.file_names_label, 1), (self.file_names, 1),
                 (self.copy_button, 2)],
                self.butt_layout,
                self.butt_layout2,
            )
        elif self.width() > 800 and self.butt_layout2.count() != 0:
            self._move_widgets(
                [(self.file_names_label, 1), (self.file_names, 1),
                 (self.copy_button, 2)],
                self.butt_layout2,
                self.butt_layout,
            )
コード例 #4
0
ファイル: logger.py プロジェクト: locolucco209/MongoScraper
class LogViewerDialog(DialogBase):
    """Logger widget."""

    def __init__(
        self,
        parent=None,
        log_folder=LOG_FOLDER,
        log_filename=LOG_FILENAME,
    ):
        """
        Logger widget.

        Parameters
        ----------
        log_folder: str
            Folder where logs are located
        log_filename: str
            Basic name for the rotating log files.
        """
        super(LogViewerDialog, self).__init__(parent=parent)

        self._data = None
        self._columns = ['level', 'time', 'module', 'method', 'message']
        self._headers = [c.capitalize() for c in self._columns]
        self._log_filename = log_filename
        self._log_folder = log_folder

        # Widgets
        self.label = QLabel('Select log file:')
        self.combobox = ComboBoxBase()
        self.table_logs = QTableWidget(self)
        self.button_copy = ButtonPrimary('Copy')
        self.text_search = LineEditSearch()

        # Widget setup
        self.table_logs.setAttribute(Qt.WA_LayoutUsesWidgetRect, True)
        horizontal_header = self.table_logs.horizontalHeader()
        vertical_header = self.table_logs.verticalHeader()
        horizontal_header.setStretchLastSection(True)
        horizontal_header.setSectionResizeMode(QHeaderView.Fixed)
        vertical_header.setSectionResizeMode(QHeaderView.Fixed)

        self.table_logs.setSelectionBehavior(QTableWidget.SelectRows)
        self.table_logs.setEditTriggers(QTableWidget.NoEditTriggers)

        self.setWindowTitle('Log Viewer')
        self.setMinimumWidth(800)
        self.setMinimumHeight(500)
        self.text_search.setPlaceholderText("Search...")

        # Layouts
        top_layout = QHBoxLayout()
        top_layout.addWidget(self.label)
        top_layout.addWidget(SpacerHorizontal())
        top_layout.addWidget(self.combobox)
        top_layout.addStretch()
        top_layout.addWidget(SpacerHorizontal())
        top_layout.addWidget(self.text_search)
        top_layout.addWidget(SpacerHorizontal())
        top_layout.addWidget(self.button_copy)
        layout = QVBoxLayout()
        layout.addLayout(top_layout)
        layout.addWidget(SpacerVertical())
        layout.addWidget(self.table_logs)
        self.setLayout(layout)

        # Signals
        self.combobox.currentIndexChanged.connect(self.update_text)
        self.button_copy.clicked.connect(self.copy_item)
        self.text_search.textChanged.connect(self.filter_text)

        # Setup()
        self.setup()
        self.update_style_sheet()

    def update_style_sheet(self, style_sheet=None):
        """Update custom CSS stylesheet."""
        if style_sheet is None:
            style_sheet = load_style_sheet()
        self.setStyleSheet(style_sheet)

    def setup(self):
        """Setup widget content."""
        self.combobox.clear()
        paths = log_files(
            log_folder=self._log_folder,
            log_filename=self._log_filename,
        )
        files = [os.path.basename(p) for p in paths]
        self.combobox.addItems(files)

    def filter_text(self):
        """Search for text in the selected log file."""
        search = self.text_search.text().lower()
        for i, data in enumerate(self._data):
            if any(search in str(d).lower() for d in data.values()):
                self.table_logs.showRow(i)
            else:
                self.table_logs.hideRow(i)

    def row_data(self, row):
        """Give the current row data concatenated with spaces."""
        data = {}
        if self._data:
            length = len(self._data)
            if 0 >= row < length:
                data = self._data[row]
        return data

    def update_text(self, index):
        """Update logs based on combobox selection."""
        path = os.path.join(self._log_folder, self.combobox.currentText())
        self._data = load_log(path)

        self.table_logs.clear()
        self.table_logs.setSortingEnabled(False)
        self.table_logs.setRowCount(len(self._data))
        self.table_logs.setColumnCount(len(self._columns))
        self.table_logs.setHorizontalHeaderLabels(self._headers)

        for row, data in enumerate(self._data):
            for col, col_key in enumerate(self._columns):
                item = QTableWidgetItem(data.get(col_key, ''))
                self.table_logs.setItem(row, col, item)

        for c in [0, 2, 3]:
            self.table_logs.resizeColumnToContents(c)

        self.table_logs.resizeRowsToContents()
        self.table_logs.setSortingEnabled(True)
        self.table_logs.scrollToBottom()
        self.table_logs.scrollToTop()
        self.table_logs.sortByColumn(1, Qt.AscendingOrder)

        # Make sure there is always a selected row
        self.table_logs.setCurrentCell(0, 0)

    def copy_item(self):
        """Copy selected item to clipboard in markdown format."""
        app = QApplication.instance()
        items = self.table_logs.selectedIndexes()
        if items:
            rows = set(sorted(i.row() for i in items))
            if self._data:
                all_data = [self._data[row] for row in rows]
                dump = json.dumps(all_data, sort_keys=True, indent=4)
                app.clipboard().setText('```json\n' + dump + '\n```')
コード例 #5
0
class WndLoadQuantitativeCalibration(SecondaryWindow):

    signal_quantitative_calibration_changed = Signal()

    def __init__(self, *, gpc, gui_vars):
        super().__init__()

        # Global processing classes
        self.gpc = gpc
        # Global GUI variables (used for control of GUI state)
        self.gui_vars = gui_vars

        self.initialize()

    def initialize(self):
        self.table_header_display_names = False

        self.setWindowTitle("PyXRF: Load Quantitative Calibration")
        self.setMinimumWidth(750)
        self.setMinimumHeight(400)
        self.resize(750, 600)

        self.pb_load_calib = QPushButton("Load Calibration ...")
        self.pb_load_calib.clicked.connect(self.pb_load_calib_clicked)

        self._changes_exist = False
        self._auto_update = True
        self.cb_auto_update = QCheckBox("Auto")
        self.cb_auto_update.setCheckState(
            Qt.Checked if self._auto_update else Qt.Unchecked)
        self.cb_auto_update.stateChanged.connect(
            self.cb_auto_update_state_changed)

        self.pb_update_plots = QPushButton("Update Plots")
        self.pb_update_plots.clicked.connect(self.pb_update_plots_clicked)

        self.grp_current_scan = QGroupBox(
            "Parameters of Currently Processed Scan")

        self._distance_to_sample = 0.0
        self.le_distance_to_sample = LineEditExtended()
        le_dist_validator = QDoubleValidator()
        le_dist_validator.setBottom(0)
        self.le_distance_to_sample.setValidator(le_dist_validator)
        self._set_distance_to_sample()
        self.le_distance_to_sample.editingFinished.connect(
            self.le_distance_to_sample_editing_finished)
        self.le_distance_to_sample.focusOut.connect(
            self.le_distance_to_sample_focus_out)

        hbox = QHBoxLayout()
        hbox.addWidget(QLabel("Distance-to-sample:"))
        hbox.addWidget(self.le_distance_to_sample)
        hbox.addStretch(1)
        self.grp_current_scan.setLayout(hbox)

        self.eline_rb_exclusive = [
        ]  # Holds the list of groups of exclusive radio buttons
        self._setup_tab_widget()

        vbox = QVBoxLayout()

        hbox = QHBoxLayout()
        hbox.addWidget(self.pb_load_calib)
        hbox.addStretch(1)
        hbox.addWidget(self.cb_auto_update)
        hbox.addWidget(self.pb_update_plots)
        vbox.addLayout(hbox)

        vbox.addWidget(self.tab_widget)

        vbox.addWidget(self.grp_current_scan)

        self.setLayout(vbox)

        # Display data
        self.update_all_data()

        self._set_tooltips()

    def _setup_tab_widget(self):

        self.tab_widget = QTabWidget()
        self.loaded_standards = QWidget()
        # self.display_loaded_standards()
        self.scroll = QScrollArea()
        self.scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
        self.scroll.setWidget(self.loaded_standards)
        self.tab_widget.addTab(self.scroll, "Loaded Standards")

        self.combo_set_table_header = QComboBox()
        self.combo_set_table_header.addItems(
            ["Standard Serial #", "Standard Name"])
        self.combo_set_table_header.currentIndexChanged.connect(
            self.combo_set_table_header_index_changed)

        vbox = QVBoxLayout()
        vbox.addSpacing(5)
        hbox = QHBoxLayout()
        hbox.addWidget(QLabel("Display in table header:"))
        hbox.addWidget(self.combo_set_table_header)
        hbox.addStretch(1)
        vbox.addLayout(hbox)
        self.table = QTableWidget()
        self.table.verticalHeader().hide()
        self.table.setSelectionMode(QTableWidget.NoSelection)
        self.table.horizontalHeader().setSectionResizeMode(
            QHeaderView.ResizeToContents)
        self.table.horizontalHeader().setMinimumSectionSize(150)
        vbox.addWidget(self.table)

        self.table.setStyleSheet("QTableWidget::item{color: black;}")

        frame = QFrame()
        vbox.setContentsMargins(0, 0, 0, 0)
        frame.setLayout(vbox)

        self.tab_widget.addTab(frame, "Selected Emission Lines")

    def display_loaded_standards(self):
        calib_data = self.gpc.get_quant_calibration_data()
        calib_settings = self.gpc.get_quant_calibration_settings()

        # Create the new widget (this deletes the old widget)
        self.loaded_standards = QWidget()
        self.loaded_standards.setMinimumWidth(700)

        # Also delete references to all components
        self.frames_calib_data = []
        self.pbs_view = []
        self.pbs_remove = []

        # All 'View' buttons are added to the group in order to be connected to the same slot
        self.group_view = QButtonGroup()
        self.group_view.setExclusive(False)
        self.group_view.buttonClicked.connect(self.pb_view_clicked)
        # The same for the 'Remove' buttons
        self.group_remove = QButtonGroup()
        self.group_remove.setExclusive(False)
        self.group_remove.buttonClicked.connect(self.pb_remove_clicked)

        vbox = QVBoxLayout()

        class _LabelBlack(QLabel):
            def __init__(self, *args, **kwargs):
                super().__init__(*args, **kwargs)
                self.setStyleSheet("color: black")

        for cdata, csettings in zip(calib_data, calib_settings):
            frame = QFrame()
            frame.setFrameStyle(QFrame.StyledPanel)
            frame.setStyleSheet(
                get_background_css((200, 255, 200), widget="QFrame"))

            _vbox = QVBoxLayout()

            name = cdata["name"]  # Standard name (can be arbitrary string
            # If name is long, then print it in a separate line
            _name_is_long = len(name) > 30

            pb_view = QPushButton("View ...")
            self.group_view.addButton(pb_view)
            pb_remove = QPushButton("Remove")
            self.group_remove.addButton(pb_remove)

            # Row 1: serial, name
            serial = cdata["serial"]
            _hbox = QHBoxLayout()
            _hbox.addWidget(_LabelBlack(f"<b>Standard</b> #{serial}"))
            if not _name_is_long:
                _hbox.addWidget(_LabelBlack(f"'{name}'"))
            _hbox.addStretch(1)
            _hbox.addWidget(pb_view)
            _hbox.addWidget(pb_remove)
            _vbox.addLayout(_hbox)

            # Optional row
            if _name_is_long:
                # Wrap name if it is extemely long
                name = textwrap.fill(name, width=80)
                _hbox = QHBoxLayout()
                _hbox.addWidget(_LabelBlack("<b>Name:</b> "), 0, Qt.AlignTop)
                _hbox.addWidget(_LabelBlack(name), 0, Qt.AlignTop)
                _hbox.addStretch(1)
                _vbox.addLayout(_hbox)

            # Row 2: description
            description = textwrap.fill(cdata["description"], width=80)
            _hbox = QHBoxLayout()
            _hbox.addWidget(_LabelBlack("<b>Description:</b>"), 0, Qt.AlignTop)
            _hbox.addWidget(_LabelBlack(f"{description}"), 0, Qt.AlignTop)
            _hbox.addStretch(1)
            _vbox.addLayout(_hbox)

            # Row 3:
            incident_energy = cdata["incident_energy"]
            scaler = cdata["scaler_name"]
            detector_channel = cdata["detector_channel"]
            distance_to_sample = cdata["distance_to_sample"]
            _hbox = QHBoxLayout()
            _hbox.addWidget(
                _LabelBlack(f"<b>Incident energy, keV:</b> {incident_energy}"))
            _hbox.addWidget(_LabelBlack(f"  <b>Scaler:</b> {scaler}"))
            _hbox.addWidget(
                _LabelBlack(f"  <b>Detector channel:</b> {detector_channel}"))
            _hbox.addWidget(
                _LabelBlack(
                    f"  <b>Distance-to-sample:</b> {distance_to_sample}"))
            _hbox.addStretch(1)
            _vbox.addLayout(_hbox)

            # Row 4: file name
            fln = textwrap.fill(csettings["file_path"], width=80)
            _hbox = QHBoxLayout()
            _hbox.addWidget(_LabelBlack("<b>Source file:</b>"), 0, Qt.AlignTop)
            _hbox.addWidget(_LabelBlack(fln), 0, Qt.AlignTop)
            _hbox.addStretch(1)
            _vbox.addLayout(_hbox)

            frame.setLayout(_vbox)

            # Now the group box is added to the upper level layout
            vbox.addWidget(frame)
            vbox.addSpacing(5)
            self.frames_calib_data.append(frame)
            self.pbs_view.append(pb_view)
            self.pbs_remove.append(pb_remove)

        # Add the layout to the widget
        self.loaded_standards.setLayout(vbox)
        # ... and put the widget inside the scroll area. This will update the
        # contents of the scroll area.
        self.scroll.setWidget(self.loaded_standards)

    def display_table_header(self):
        calib_data = self.gpc.get_quant_calibration_data()
        header_by_name = self.table_header_display_names

        tbl_labels = ["Lines"]
        for n, cdata in enumerate(calib_data):
            if header_by_name:
                txt = cdata["name"]
            else:
                txt = cdata["serial"]
            txt = textwrap.fill(txt, width=20)
            tbl_labels.append(txt)

        self.table.setHorizontalHeaderLabels(tbl_labels)

    def display_standard_selection_table(self):
        calib_data = self.gpc.get_quant_calibration_data()
        self._quant_file_paths = self.gpc.get_quant_calibration_file_path_list(
        )

        brightness = 220
        table_colors = [(255, brightness, brightness),
                        (brightness, 255, brightness)]

        # Disconnect all radio button signals before clearing the table
        for bgroup in self.eline_rb_exclusive:
            bgroup.buttonToggled.disconnect(self.rb_selection_toggled)

        # This list will hold radio button groups for horizontal rows
        #   Those are exclusive groups. They are not going to be
        #   used directly, but they must be kept alive in order
        #   for the radiobuttons to work properly. Most of the groups
        #   will contain only 1 radiobutton, which will always remain checked.
        self.eline_rb_exclusive = []
        # The following list will contain the list of radio buttons for each
        #   row. If there is no radiobutton in a position, then the element is
        #   set to None.
        # N rows: the number of emission lines, N cols: the number of standards
        self.eline_rb_lists = []

        self.table.clear()

        if not calib_data:
            self.table.setRowCount(0)
            self.table.setColumnCount(0)
        else:
            # Create the sorted list of available element lines
            line_set = set()
            for cdata in calib_data:
                ks = list(cdata["element_lines"].keys())
                line_set.update(list(ks))
            self.eline_list = list(line_set)
            self.eline_list.sort()

            for n in range(len(self.eline_list)):
                self.eline_rb_exclusive.append(QButtonGroup())
                self.eline_rb_lists.append([None] * len(calib_data))

            self.table.setColumnCount(len(calib_data) + 1)
            self.table.setRowCount(len(self.eline_list))
            self.display_table_header()

            for n, eline in enumerate(self.eline_list):

                rgb = table_colors[n % 2]

                item = QTableWidgetItem(eline)
                item.setTextAlignment(Qt.AlignCenter)
                item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                item.setBackground(QBrush(QColor(*rgb)))
                self.table.setItem(n, 0, item)

                for ns, cdata in enumerate(calib_data):
                    q_file_path = self._quant_file_paths[
                        ns]  # Used to identify standard
                    if eline in cdata["element_lines"]:
                        rb = QRadioButton()
                        if self.gpc.get_quant_calibration_is_eline_selected(
                                eline, q_file_path):
                            rb.setChecked(True)

                        rb.setStyleSheet("color: black")

                        self.eline_rb_lists[n][ns] = rb
                        # self.eline_rb_by_standard[ns].addButton(rb)
                        self.eline_rb_exclusive[n].addButton(rb)

                        item = QWidget()
                        item_hbox = QHBoxLayout(item)
                        item_hbox.addWidget(rb)
                        item_hbox.setAlignment(Qt.AlignCenter)
                        item_hbox.setContentsMargins(0, 0, 0, 0)

                        item.setStyleSheet(get_background_css(rgb))

                        # Generate tooltip
                        density = cdata["element_lines"][eline]["density"]
                        fluorescence = cdata["element_lines"][eline][
                            "fluorescence"]
                        ttip = f"Fluorescence (F): {fluorescence:12g}\nDensity (D): {density:12g}\n"
                        # Avoid very small values of density (probably zero)
                        if abs(density) > 1e-30:
                            ttip += f"F/D: {fluorescence/density:12g}"

                        item.setToolTip(ttip)

                        self.table.setCellWidget(n, ns + 1, item)
                    else:
                        # There is no radio button, but we still need to fill the cell
                        item = QTableWidgetItem("")
                        item.setFlags(item.flags() & ~Qt.ItemIsEditable)
                        item.setBackground(QBrush(QColor(*rgb)))
                        self.table.setItem(n, ns + 1, item)

            # Now the table is set (specifically radio buttons).
            # So we can connect the button groups with the event processing function
            for bgroup in self.eline_rb_exclusive:
                bgroup.buttonToggled.connect(self.rb_selection_toggled)

    @Slot()
    def update_all_data(self):
        self.display_loaded_standards()
        self.display_standard_selection_table()
        self._set_distance_to_sample()

    def _set_distance_to_sample(self):
        """Set 'le_distance_to_sample` without updating maps"""
        distance_to_sample = self.gpc.get_quant_calibration_distance_to_sample(
        )
        if distance_to_sample is None:
            distance_to_sample = 0.0
        self._distance_to_sample = distance_to_sample
        self._set_le_distance_to_sample(distance_to_sample)

    def _set_tooltips(self):
        set_tooltip(self.pb_load_calib,
                    "Load <b>calibration data</b> from JSON file.")
        set_tooltip(
            self.cb_auto_update,
            "Automatically <b>update the plots</b> when changes are made. "
            "If unchecked, then button <b>Update Plots</b> must be pressed "
            "to update the plots. Automatic update is often undesirable "
            "when large maps are displayed and multiple changes to parameters "
            "are made.",
        )
        set_tooltip(
            self.pb_update_plots,
            "<b>Update plots</b> based on currently selected parameters.")
        set_tooltip(
            self.le_distance_to_sample,
            "Distance between <b>the sample and the detector</b>. The ratio between of the distances "
            "during calibration and measurement is used to scale computed concentrations. "
            "If distance-to-sample is 0 for calibration or measurement, then no scaling is performed.",
        )
        set_tooltip(
            self.combo_set_table_header,
            "Use <b>Serial Number</b> or <b>Name</b> of the calibration standard in the header of the table",
        )
        set_tooltip(
            self.table,
            "Use Radio Buttons to select the <b>source of calibration data</b> for each emission line. "
            "This feature is needed if multiple loaded calibration files have data on the same "
            "emission line.",
        )

    def update_widget_state(self, condition=None):
        # Update the state of the menu bar
        state = not self.gui_vars["gui_state"]["running_computations"]
        self.setEnabled(state)

        # Hide the window if required by the program state
        state_xrf_map_exists = self.gui_vars["gui_state"][
            "state_xrf_map_exists"]
        if not state_xrf_map_exists:
            self.hide()

        if condition == "tooltips":
            self._set_tooltips()

    def cb_auto_update_state_changed(self, state):
        self._auto_update = state
        self.pb_update_plots.setEnabled(not state)
        # If changes were made, apply the changes while switching to 'auto' mode
        if state and self._changes_exist:
            self._update_maps_auto()

    def pb_update_plots_clicked(self):
        self._update_maps()

    def pb_load_calib_clicked(self):
        current_dir = self.gpc.get_current_working_directory()
        file_name = QFileDialog.getOpenFileName(
            self, "Select File with Quantitative Calibration Data",
            current_dir, "JSON (*.json);; All (*)")
        file_name = file_name[0]
        if file_name:
            try:
                logger.debug(
                    f"Loading quantitative calibration from file: '{file_name}'"
                )
                self.gpc.load_quantitative_calibration_data(file_name)
                self.update_all_data()
                self._update_maps_auto()
            except Exception:
                msg = "The selected JSON file has incorrect format. Select a different file."
                msgbox = QMessageBox(QMessageBox.Critical,
                                     "Data Loading Error",
                                     msg,
                                     QMessageBox.Ok,
                                     parent=self)
                msgbox.exec()

    def pb_view_clicked(self, button):
        try:
            n_standard = self.pbs_view.index(button)
            calib_settings = self.gpc.get_quant_calibration_settings()
            file_path = calib_settings[n_standard]["file_path"]
            calib_preview = self.gpc.get_quant_calibration_text_preview(
                file_path)
            dlg = DialogViewCalibStandard(None,
                                          file_path=file_path,
                                          calib_preview=calib_preview)
            dlg.exec()
        except ValueError:
            logger.error(
                "'View' button was pressed, but not found in the list of buttons"
            )

    def pb_remove_clicked(self, button):
        try:
            n_standard = self.pbs_remove.index(button)
            calib_settings = self.gpc.get_quant_calibration_settings()
            file_path = calib_settings[n_standard]["file_path"]
            self.gpc.quant_calibration_remove_entry(file_path)
            self.update_all_data()
            self._update_maps_auto()
        except ValueError:
            logger.error(
                "'Remove' button was pressed, but not found in the list of buttons"
            )

    def rb_selection_toggled(self, button, checked):
        if checked:
            # Find the button in 2D list 'self.eline_rb_lists'
            button_found = False
            for nr, rb_list in enumerate(self.eline_rb_lists):
                try:
                    nc = rb_list.index(button)
                    button_found = True
                    break
                except ValueError:
                    pass

            if button_found:
                eline = self.eline_list[nr]
                n_standard = nc
                file_path = self._quant_file_paths[n_standard]
                self.gpc.set_quant_calibration_select_eline(eline, file_path)
                self._update_maps_auto()
            else:
                # This should never happen
                logger.error(
                    "Selection radio button was pressed, but not found in the list"
                )

    def combo_set_table_header_index_changed(self, index):
        self.table_header_display_names = bool(index)
        self.display_table_header()

    def le_distance_to_sample_editing_finished(self):
        distance_to_sample = float(self.le_distance_to_sample.text())
        if distance_to_sample != self._distance_to_sample:
            self._distance_to_sample = distance_to_sample
            self.gpc.set_quant_calibration_distance_to_sample(
                distance_to_sample)
            self._update_maps_auto()

    def le_distance_to_sample_focus_out(self):
        try:
            float(self.le_distance_to_sample.text())
        except ValueError:
            # If the text can not be interpreted to float, then replace the text with the old value
            self._set_le_distance_to_sample(self._distance_to_sample)

    def _set_le_distance_to_sample(self, distance_to_sample):
        self.le_distance_to_sample.setText(f"{distance_to_sample:.12g}")

    def _update_maps_auto(self):
        """Update maps only if 'auto' update is ON. Used as a 'filter'
        to prevent extra plot updates."""
        self._changes_exist = True
        if self._auto_update:
            self._update_maps()

    def _update_maps(self):
        """Upload the selections (limit table) and update plot"""
        self._changes_exist = False
        self._redraw_maps()
        # Emit signal only after the maps are redrawn. This should change
        #   ranges in the respective controls for the plots
        self.signal_quantitative_calibration_changed.emit()

    def _redraw_maps(self):
        # We don't emit any signals here, but we don't really need to.
        logger.debug("Redrawing RGB XRF Maps")
        self.gpc.compute_map_ranges()
        self.gpc.redraw_maps()
        self.gpc.compute_rgb_map_ranges()
        self.gpc.redraw_rgb_maps()