Exemplo n.º 1
0
    def __init__(self):
        super().__init__()
        self.setWindowTitle("muSync - Accounts")
        self.setModal(True)

        dialogLayout = QVBoxLayout(self)

        accountsList = QListWidget()
        accountsList.setObjectName("accountsList")
        dialogLayout.addWidget(accountsList)

        buttonsLayout = QHBoxLayout()
        addButton = QPushButton(QIcon.fromTheme("list-resource-add"),
                                "&Add account")
        addButton.clicked.connect(lambda: self.__show_modules())
        buttonsLayout.addWidget(addButton)
        delButton = QPushButton(QIcon.fromTheme("edit-delete"),
                                "&Remove account")
        delButton.clicked.connect(lambda: self.__del_account(accountsList))
        delButton.setDisabled(True)
        buttonsLayout.addWidget(delButton)
        okButton = QPushButton(QIcon.fromTheme("dialog-ok"), "&Select account")
        okButton.clicked.connect(lambda: self.__select_account(accountsList))
        okButton.setDisabled(True)
        buttonsLayout.addWidget(okButton)
        cancelButton = QPushButton(QIcon.fromTheme("dialog-cancel"), "&Cancel")
        cancelButton.clicked.connect(self.close)
        buttonsLayout.addWidget(cancelButton)
        dialogLayout.addLayout(buttonsLayout)
        accountsList.itemSelectionChanged.connect(lambda: okButton.setDisabled(
            True if accountsList.selectedIndexes() == [] else False))
        accountsList.itemSelectionChanged.connect(
            lambda: delButton.setDisabled(True if accountsList.selectedIndexes(
            ) == [] else False))

        accounts = QSettings()
        accounts.beginGroup("accounts")
        for account_id in accounts.childKeys():
            module_name = accounts.value(account_id)

            if module_name not in modules.modules:
                pass  # TODO

            account = modules.create_object(module_name)
            account.setId(account_id)
            account.initialize()

            accountListItem = QListWidgetItem(account.getName())
            accountListItem.account = account
            accountsList.addItem(accountListItem)

        accountsList.itemDoubleClicked.connect(
            lambda: self.__select_account(accountsList))
Exemplo n.º 2
0
class FieldAttrsDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setLayout(QGridLayout())
        self.setWindowTitle("Edit Attributes")

        self.list_widget = QListWidget()
        self.list_widget.setMinimumSize(800, 600)
        self.add_button = QPushButton("Add attr")
        self.add_button.clicked.connect(self.__add_attr)
        self.remove_button = QPushButton("Remove attr")
        self.remove_button.clicked.connect(self._remove_attrs)

        self.layout().addWidget(self.list_widget, 0, 0, 2, 1)
        self.layout().addWidget(self.add_button, 0, 1)
        self.layout().addWidget(self.remove_button, 1, 1)

    def fill_existing_attrs(self, existing_dataset: h5py.Dataset):
        for name, value in existing_dataset.attrs.items():
            if name not in ATTRS_BLACKLIST:
                frame = FieldAttrFrame(name, value)
                self._add_attr(existing_frame=frame)

    def __add_attr(self):
        """
        Only used for button presses. Any additional arguments from the signal are ignored.
        """
        self._add_attr()

    def _add_attr(self, existing_frame=None):
        item = QListWidgetItem()
        self.list_widget.addItem(item)
        frame = existing_frame if existing_frame is not None else FieldAttrFrame(
        )
        item.setSizeHint(frame.sizeHint())
        self.list_widget.setItemWidget(item, frame)

    def _remove_attrs(self):
        for index in self.list_widget.selectedIndexes():
            self.list_widget.takeItem(index.row())

    def get_attrs(self):
        attrs_dict = {}
        for index in range(self.list_widget.count()):
            item = self.list_widget.item(index)
            widget = self.list_widget.itemWidget(item)
            attrs_dict[widget.value[0]] = widget.value[1]
        return attrs_dict
Exemplo n.º 3
0
 def __init__(self, parent):
     super().__init__()
     self.setWindowTitle("muSync - Sources")
     self.setModal(True)
     dialogLayout = QVBoxLayout(self)
     sourcesList = QListWidget()
     dialogLayout.addWidget(sourcesList)
     buttonsLayout = QHBoxLayout()
     okButton = QPushButton(QIcon.fromTheme("dialog-ok"), "&Ok")
     okButton.clicked.connect(lambda: self.__add_account(sourcesList))
     okButton.setDisabled(True)
     buttonsLayout.addWidget(okButton)
     cancelButton = QPushButton(QIcon.fromTheme("dialog-cancel"), "&Cancel")
     cancelButton.clicked.connect(self.close)
     buttonsLayout.addWidget(cancelButton)
     dialogLayout.addLayout(buttonsLayout)
     for source in modules.modules.items():
         sourceItem = QListWidgetItem(source[1])
         sourceItem.slug = source[0]
         sourcesList.addItem(sourceItem)
     sourcesList.itemSelectionChanged.connect(lambda: okButton.setDisabled(
         True if sourcesList.selectedIndexes() == [] else False))
     sourcesList.itemDoubleClicked.connect(
         lambda: self.__add_account(sourcesList))
Exemplo n.º 4
0
class EMMAResolverPanel(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent=parent, f=Qt.Window)
        self.setWindowTitle(self.tr("EMMA Resolver"))
        self.SUPPORTED_DISTS = \
            [(DistributionType.Nonparametric, self.tr("Nonparametric")),
             (DistributionType.Normal, self.tr("Normal")),
             (DistributionType.Weibull, self.tr("Weibull")),
             (DistributionType.SkewNormal, self.tr("Skew Normal"))]

        self.init_ui()
        self.normal_msg = QMessageBox(self)
        self.__dataset = None  # type: GrainSizeDataset
        self.__result_list = []  # type: list[EMMAResult]
        self.neural_setting = NNResolverSettingWidget(parent=self)
        self.neural_setting.setting = NNResolverSetting(min_niter=800,
                                                        max_niter=1200,
                                                        tol=1e-6,
                                                        ftol=1e-8,
                                                        lr=5e-2)
        self.load_dialog = LoadDatasetDialog(parent=self)
        self.load_dialog.dataset_loaded.connect(self.on_dataset_loaded)
        self.file_dialog = QFileDialog(parent=self)
        self.emma_result_chart = EMMAResultChart(toolbar=True)
        self.emma_summary_chart = EMMASummaryChart(toolbar=True)

    def init_ui(self):
        self.setAttribute(Qt.WA_StyledBackground, True)
        self.main_layout = QGridLayout(self)
        # self.main_layout.setContentsMargins(0, 0, 0, 0)
        # control group
        self.control_group = QGroupBox(self.tr("Control"))
        self.control_layout = QGridLayout(self.control_group)
        self.main_layout.addWidget(self.control_group, 0, 0)
        self.load_dataset_button = QPushButton(qta.icon("fa.database"),
                                               self.tr("Load Dataset"))
        self.load_dataset_button.clicked.connect(self.on_load_clicked)
        self.control_layout.addWidget(self.load_dataset_button, 0, 0, 1, 2)
        self.configure_button = QPushButton(qta.icon("fa.gears"),
                                            self.tr("Configure Algorithm"))
        self.configure_button.clicked.connect(self.on_configure_clicked)
        self.control_layout.addWidget(self.configure_button, 1, 0, 1, 2)
        self.n_samples_label = QLabel(self.tr("N<sub>samples</sub>"))
        self.n_samples_display = QLabel(self.tr("Unknown"))
        self.control_layout.addWidget(self.n_samples_label, 2, 0)
        self.control_layout.addWidget(self.n_samples_display, 2, 1)
        self.distribution_label = QLabel(self.tr("Distribution Type"))
        self.distribution_combo_box = QComboBox()
        self.distribution_combo_box.addItems(
            [name for _, name in self.SUPPORTED_DISTS])
        self.control_layout.addWidget(self.distribution_label, 3, 0)
        self.control_layout.addWidget(self.distribution_combo_box, 3, 1)
        self.min_n_members_label = QLabel("Minimum N<sub>members</sub>")
        self.min_n_members_input = QSpinBox()
        self.min_n_members_input.setRange(1, 10)
        self.control_layout.addWidget(self.min_n_members_label, 4, 0)
        self.control_layout.addWidget(self.min_n_members_input, 4, 1)
        self.max_n_members_label = QLabel("Maximum N<sub>members</sub>")
        self.max_n_members_input = QSpinBox()
        self.max_n_members_input.setRange(1, 10)
        self.max_n_members_input.setValue(10)
        self.control_layout.addWidget(self.max_n_members_label, 5, 0)
        self.control_layout.addWidget(self.max_n_members_input, 5, 1)
        self.perform_button = QPushButton(qta.icon("fa.play-circle"),
                                          self.tr("Perform"))
        self.perform_button.clicked.connect(self.on_perform_clicked)
        self.perform_button.setEnabled(False)
        self.perform_with_customized_ems_button = QPushButton(
            qta.icon("fa.play-circle"), self.tr("Perform With Customized EMs"))
        self.perform_with_customized_ems_button.clicked.connect(
            self.on_perform_with_customized_ems)
        self.perform_with_customized_ems_button.setEnabled(False)
        self.control_layout.addWidget(self.perform_button, 6, 0, 1, 2)
        self.control_layout.addWidget(self.perform_with_customized_ems_button,
                                      7, 0, 1, 2)
        self.progress_bar = QProgressBar()
        self.progress_bar.setFormat(self.tr("EMMA Progress"))
        self.control_layout.addWidget(self.progress_bar, 8, 0, 1, 2)

        self.result_group = QGroupBox(self.tr("Result"))
        self.result_layout = QGridLayout(self.result_group)
        self.main_layout.addWidget(self.result_group, 0, 1)
        self.result_list_widget = QListWidget()
        self.result_layout.addWidget(self.result_list_widget, 0, 0, 1, 2)
        self.remove_result_button = QPushButton(qta.icon("mdi.delete"),
                                                self.tr("Remove"))
        self.remove_result_button.clicked.connect(self.on_remove_clicked)
        self.show_result_button = QPushButton(qta.icon("fa.area-chart"),
                                              self.tr("Show"))
        self.show_result_button.clicked.connect(self.on_show_clicked)
        self.load_dump_button = QPushButton(qta.icon("fa.database"),
                                            self.tr("Load Dump"))
        self.load_dump_button.clicked.connect(self.on_load_dump_clicked)
        self.save_button = QPushButton(qta.icon("fa.save"), self.tr("Save"))
        self.save_button.clicked.connect(self.on_save_clicked)
        self.remove_result_button.setEnabled(False)
        self.show_result_button.setEnabled(False)
        self.save_button.setEnabled(False)
        self.result_layout.addWidget(self.remove_result_button, 1, 0)
        self.result_layout.addWidget(self.show_result_button, 1, 1)
        self.result_layout.addWidget(self.load_dump_button, 2, 0)
        self.result_layout.addWidget(self.save_button, 2, 1)

    def show_message(self, title: str, message: str):
        self.normal_msg.setWindowTitle(title)
        self.normal_msg.setText(message)
        self.normal_msg.exec_()

    def show_info(self, message: str):
        self.show_message(self.tr("Info"), message)

    def show_warning(self, message: str):
        self.show_message(self.tr("Warning"), message)

    def show_error(self, message: str):
        self.show_message(self.tr("Error"), message)

    def on_dataset_loaded(self, dataset: GrainSizeDataset):
        self.__dataset = dataset
        self.n_samples_display.setText(str(self.__dataset.n_samples))
        self.perform_button.setEnabled(True)
        self.perform_with_customized_ems_button.setEnabled(True)

    def on_load_clicked(self):
        self.load_dialog.show()

    def on_configure_clicked(self):
        self.neural_setting.show()

    @property
    def distribution_type(self) -> DistributionType:
        distribution_type, _ = self.SUPPORTED_DISTS[
            self.distribution_combo_box.currentIndex()]
        return distribution_type

    @property
    def n_members_list(self):
        min_n = self.min_n_members_input.value()
        max_n = self.max_n_members_input.value()
        if min_n > max_n:
            min_n, max_n = max_n, min_n
        return list(range(min_n, max_n + 1, 1))

    @property
    def n_results(self) -> int:
        return len(self.__result_list)

    @property
    def selected_index(self):
        indexes = self.result_list_widget.selectedIndexes()
        if len(indexes) == 0:
            return 0
        else:
            return indexes[0].row()

    @property
    def selected_result(self):
        if self.n_results == 0:
            None
        else:
            return self.__result_list[self.selected_index]

    def on_perform_clicked(self):
        if self.__dataset is None:
            self.show_error(self.tr("Dataset has not been loaded."))
            return

        self.perform_button.setEnabled(False)
        self.perform_with_customized_ems_button.setEnabled(False)
        resolver = EMMAResolver()
        resolver_setting = self.neural_setting.setting
        results = []
        n_members_list = self.n_members_list
        self.progress_bar.setRange(0, len(n_members_list))
        self.progress_bar.setValue(0)
        self.progress_bar.setFormat(self.tr("Performing EMMA [%v/%m]"))
        QCoreApplication.processEvents()
        for i, n_members in enumerate(n_members_list):
            result = resolver.try_fit(self.__dataset, self.distribution_type,
                                      n_members, resolver_setting)
            results.append(result)
            self.progress_bar.setValue(i + 1)
            QCoreApplication.processEvents()

        self.add_results(results)
        self.progress_bar.setFormat(self.tr("Finished"))
        self.perform_button.setEnabled(True)
        self.perform_with_customized_ems_button.setEnabled(True)
        if len(results) > 1:
            self.emma_summary_chart.show_distances(results)
            self.emma_summary_chart.show()

    def on_perform_with_customized_ems(self):
        if self.__dataset is None:
            self.show_error(self.tr("Dataset has not been loaded."))
            return

        filename, _ = self.file_dialog.getOpenFileName(
            self,
            self.
            tr("Choose a excel file which contains the customized EMs at the first sheet"
               ), None, f"{self.tr('Microsoft Excel')} (*.xlsx)")
        if filename is None or filename == "":
            return
        try:
            wb = openpyxl.load_workbook(filename,
                                        read_only=True,
                                        data_only=True)
            ws = wb[wb.sheetnames[0]]
            raw_data = [[value for value in row] for row in ws.values]
            classes_μm = np.array(raw_data[0][1:], dtype=np.float64)
            classes_φ = convert_μm_to_φ(classes_μm)
            em_distributions = [
                np.array(row[1:], dtype=np.float64) for row in raw_data[1:]
            ]
        except Exception as e:
            self.show_error(
                self.tr(
                    "Error raised while loading the customized EMs.\n    {0}").
                format(e.__str__()))
            return
        if len(classes_μm) < 10:
            self.show_error(
                self.tr("The length of grain-size classes is too less."))
            return
        for i in range(len(classes_μm) - 1):
            if classes_μm[i + 1] <= classes_μm[i]:
                self.show_error(
                    self.tr("The grain-size classes is not incremental."))
                return
        if np.any(np.isnan(classes_μm)):
            self.show_error(
                self.tr(
                    "There is at least one nan value in grain-size classes."))
            return
        if len(em_distributions) > 10:
            self.show_error(
                self.
                tr("There are more than 10 customized EMs in the first sheet, please check."
                   ))
            return
        for distribution in em_distributions:
            if len(classes_μm) != len(distribution):
                self.show_error(
                    self.
                    tr("Some distributions of customized EMs have different length with the grain-size classes."
                       ))
                return
            if np.any(np.isnan(distribution)):
                self.show_error(
                    self.
                    tr("There is at least one nan value in the frequceny distributions of EMs."
                       ))
                return
            if abs(np.sum(distribution) - 1.0) > 0.05:
                self.show_error(
                    self.
                    tr("The sum of some distributions of customized EMs are not equal to 1."
                       ))
                return

        self.perform_button.setEnabled(False)
        self.perform_with_customized_ems_button.setEnabled(False)
        resolver = EMMAResolver()
        resolver_setting = self.neural_setting.setting

        self.progress_bar.setRange(0, 1)
        self.progress_bar.setValue(0)
        self.progress_bar.setFormat(self.tr("Performing EMMA [%v/%m]"))
        QCoreApplication.processEvents()
        try:
            result = resolver.try_fit_with_fixed_ems(self.__dataset, classes_φ,
                                                     em_distributions,
                                                     resolver_setting)
            self.progress_bar.setValue(1)
            self.add_results([result])
            self.progress_bar.setFormat(self.tr("Finished"))
        except Exception as e:
            self.show_error(
                self.tr("Error raised while fitting.\n    {0}").format(
                    e.__str__()))
            self.progress_bar.setFormat(self.tr("Failed"))
        QCoreApplication.processEvents()
        self.perform_button.setEnabled(True)
        self.perform_with_customized_ems_button.setEnabled(True)

    def get_distribution_name(self, distribution_type: DistributionType):
        if distribution_type == DistributionType.Nonparametric:
            return self.tr("Nonparametric")
        elif distribution_type == DistributionType.Normal:
            return self.tr("Normal")
        elif distribution_type == DistributionType.Weibull:
            return self.tr("Weibull")
        elif distribution_type == DistributionType.SkewNormal:
            return self.tr("Skew Normal")
        elif distribution_type == DistributionType.Customized:
            return self.tr("Customized")
        else:
            raise NotImplementedError(distribution_type)

    def get_result_name(self, result: EMMAResult):
        return f"[{self.get_distribution_name(result.distribution_type)}]-[{result.n_members} EM(s)]"

    def add_results(self, results: typing.List[EMMAResult]):
        if self.n_results == 0:
            self.remove_result_button.setEnabled(True)
            self.show_result_button.setEnabled(True)
            self.save_button.setEnabled(True)

        self.__result_list.extend(results)
        self.result_list_widget.addItems(
            [self.get_result_name(result) for result in results])

    def on_remove_clicked(self):
        if self.n_results == 0:
            return
        else:
            index = self.selected_index
            self.__result_list.pop(index)
            self.result_list_widget.takeItem(index)

        if self.n_results == 0:
            self.remove_result_button.setEnabled(False)
            self.show_result_button.setEnabled(False)
            self.save_button.setEnabled(False)

    def on_show_clicked(self):
        result = self.selected_result
        if result is None:
            return
        else:
            self.emma_result_chart.show_result(result)
            self.emma_result_chart.show()

    def on_load_dump_clicked(self):
        filename, _ = self.file_dialog.getOpenFileName(
            self, self.tr("Select the dump file of the EMMA result(s)"), None,
            f"{self.tr('Binary Dump')} (*.dump)")
        if filename is None or filename == "":
            return
        with open(filename, "rb") as f:
            results = pickle.load(f)
            invalid = False
            if isinstance(results, list):
                for result in results:
                    if not isinstance(result, EMMAResult):
                        invalid = True
                        break
            else:
                invalid = True
            if invalid:
                self.show_error(
                    self.tr("The dump file does not contain any EMMA result."))
                return
            else:
                self.add_results(results)

    def save_result_excel(self, filename: str, result: EMMAResult):
        # get the mode size of each end-members
        modes = [(i, result.dataset.classes_μm[np.unravel_index(
            np.argmax(result.end_members[i]), result.end_members[i].shape)])
                 for i in range(result.n_members)]
        # sort them by mode size
        modes.sort(key=lambda x: x[1])
        wb = openpyxl.Workbook()
        prepare_styles(wb)

        ws = wb.active
        ws.title = self.tr("README")
        description = \
            """
            This Excel file was generated by QGrain ({0}).

            Please cite:
            Liu, Y., Liu, X., Sun, Y., 2021. QGrain: An open-source and easy-to-use software for the comprehensive analysis of grain size distributions. Sedimentary Geology 423, 105980. https://doi.org/10.1016/j.sedgeo.2021.105980

            It contanins three sheets:
            1. The first sheet is the dataset which was used to perform the EMMA algorithm.
            2. The second sheet is used to put the distributions of all end-members.
            3. The third sheet is the end-member fractions of all samples.

            This EMMA algorithm was implemented by QGrian, using the famous machine learning framework, PyTorch.

            EMMA algorithm details
                N_samples: {1},
                Distribution Type: {2},
                N_members: {3},
                N_iterations: {4},
                Spent Time: {5} s,

                Computing Device: {6},
                Distance: {7},
                Minimum N_iterations: {8},
                Maximum N_iterations: {9},
                Learning Rate: {10},
                eps: {11},
                tol: {12},
                ftol: {13}

            """.format(QGRAIN_VERSION,
                    result.dataset.n_samples,
                    result.distribution_type.name,
                    result.n_members,
                    result.n_iterations,
                    result.time_spent,
                    result.resolver_setting.device,
                    result.resolver_setting.distance,
                    result.resolver_setting.min_niter,
                    result.resolver_setting.max_niter,
                    result.resolver_setting.lr,
                    result.resolver_setting.eps,
                    result.resolver_setting.tol,
                    result.resolver_setting.ftol)

        def write(row, col, value, style="normal_light"):
            cell = ws.cell(row + 1, col + 1, value=value)
            cell.style = style

        lines_of_desc = description.split("\n")
        for row, line in enumerate(lines_of_desc):
            write(row, 0, line, style="description")
        ws.column_dimensions[column_to_char(0)].width = 200

        ws = wb.create_sheet(self.tr("Dataset"))
        write(0, 0, self.tr("Sample Name"), style="header")
        ws.column_dimensions[column_to_char(0)].width = 16
        for col, value in enumerate(result.dataset.classes_μm, 1):
            write(0, col, value, style="header")
            ws.column_dimensions[column_to_char(col)].width = 10
        for row, sample in enumerate(result.dataset.samples, 1):
            if row % 2 == 0:
                style = "normal_dark"
            else:
                style = "normal_light"
            write(row, 0, sample.name, style=style)
            for col, value in enumerate(sample.distribution, 1):
                write(row, col, value, style=style)
            QCoreApplication.processEvents()

        ws = wb.create_sheet(self.tr("End-members"))
        write(0, 0, self.tr("End-member"), style="header")
        ws.column_dimensions[column_to_char(0)].width = 16
        for col, value in enumerate(result.dataset.classes_μm, 1):
            write(0, col, value, style="header")
            ws.column_dimensions[column_to_char(col)].width = 10
        for row, (index, _) in enumerate(modes, 1):
            if row % 2 == 0:
                style = "normal_dark"
            else:
                style = "normal_light"
            write(row, 0, f"EM{row}", style=style)
            for col, value in enumerate(result.end_members[index], 1):
                write(row, col, value, style=style)
            QCoreApplication.processEvents()

        ws = wb.create_sheet(self.tr("Fractions"))
        write(0, 0, self.tr("Sample Name"), style="header")
        ws.column_dimensions[column_to_char(0)].width = 16
        for i in range(result.n_members):
            write(0, i + 1, f"EM{i+1}", style="header")
            ws.column_dimensions[column_to_char(i + 1)].width = 10
        for row, fractions in enumerate(result.fractions, 1):
            if row % 2 == 0:
                style = "normal_dark"
            else:
                style = "normal_light"
            write(row, 0, result.dataset.samples[row - 1].name, style=style)
            for col, (index, _) in enumerate(modes, 1):
                write(row, col, fractions[index], style=style)
            QCoreApplication.processEvents()

        wb.save(filename)
        wb.close()

    def on_save_clicked(self):
        if self.n_results == 0:
            self.show_warning(
                self.tr("There is not an EMMA result in the list."))
            return

        filename, _ = self.file_dialog.getSaveFileName(
            self,
            self.tr("Choose a filename to save the EMMA result(s) in list"),
            None,
            f"{self.tr('Binary Dump')} (*.dump);;{self.tr('Microsoft Excel')} (*.xlsx)"
        )
        if filename is None or filename == "":
            return
        _, ext = os.path.splitext(filename)

        if ext == ".dump":
            with open(filename, "wb") as f:
                pickle.dump(self.__result_list, f)
                self.show_info(self.tr("All results in list has been saved."))
        elif ext == ".xlsx":
            try:
                result = self.selected_result
                self.save_result_excel(filename, result)
                self.show_info(self.tr("The selected result has been saved."))
            except Exception as e:
                self.show_error(
                    self.tr(
                        "Error raised while saving it to Excel file.\n    {0}"
                    ).format(e.__str__()))
                return
Exemplo n.º 5
0
class FieldAttrsDialog(QDialog):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setLayout(QGridLayout())
        self.setWindowTitle("Edit Attributes")

        self.list_widget = QListWidget()
        self.list_widget.setMinimumSize(800, 600)
        self.add_button = QPushButton("Add attr")
        self.add_button.clicked.connect(self.__add_attr)
        self.remove_button = QPushButton("Remove attr")
        self.remove_button.clicked.connect(self._remove_attrs)

        self.layout().addWidget(self.list_widget, 0, 0, 2, 1)
        self.layout().addWidget(self.add_button, 0, 1)
        self.layout().addWidget(self.remove_button, 1, 1)

    def fill_existing_attrs(self, existing_dataset: Dataset):
        for attr in existing_dataset.attributes:
            if attr.name not in ATTRS_EXCLUDELIST:
                frame = FieldAttrFrame(attr)
                self._add_attr(existing_frame=frame)

    def __add_attr(self):
        """
        Only used for button presses. Any additional arguments from the signal are ignored.
        """
        self._add_attr()

    def _add_attr(self, existing_frame=None):
        item = QListWidgetItem()
        self.list_widget.addItem(item)
        frame = existing_frame if existing_frame is not None else FieldAttrFrame(
        )
        item.setSizeHint(frame.sizeHint())
        self._setup_attribute_name_validator(frame)
        self.list_widget.setItemWidget(item, frame)

    def _remove_attrs(self):
        for index in self.list_widget.selectedIndexes():
            self.list_widget.takeItem(index.row())

    def get_attrs(self):
        attrs_list = []
        for index in range(self.list_widget.count()):
            item = self.list_widget.item(index)
            widget = self.list_widget.itemWidget(item)
            if widget:
                attrs_list.append((widget.name, widget.value, widget.dtype))
        return attrs_list

    def get_attr_names(self):
        return [item[0] for item in self.get_attrs()]

    def _setup_attribute_name_validator(self, frame):
        frame.attr_name_lineedit.setValidator(
            AttributeNameValidator(self.get_attr_names))
        frame.attr_name_lineedit.validator().is_valid.connect(
            partial(
                validate_line_edit,
                frame.attr_name_lineedit,
                tooltip_on_accept="Attribute name is valid.",
                tooltip_on_reject="Attribute name is not valid",
            ))