diff --git a/src/ascii_dialog/col_editor.py b/src/ascii_dialog/col_editor.py new file mode 100644 index 0000000000..901338a021 --- /dev/null +++ b/src/ascii_dialog/col_editor.py @@ -0,0 +1,88 @@ +from PySide6.QtGui import QRegularExpressionValidator +from PySide6.QtWidgets import QComboBox, QHBoxLayout, QWidget +from PySide6.QtCore import Slot, Signal +from sasdata.quantities.units import NamedUnit +from sasdata.ascii_reader_metadata import pairings +from column_unit import ColumnUnit +from typing import cast + +class ColEditor(QWidget): + """An editor widget which allows the user to specify the columns of the data + from a set of options based on which dataset type has been selected.""" + column_changed = Signal() + + @Slot() + def onColumnUpdate(self): + column_changed = cast(ColumnUnit, self.sender()) + pairing = pairings.get(column_changed.currentColumn) + if not pairing is None: + for col_unit in self.option_widgets: + # Second condition is important because otherwise, this event will keep being called, and the GUI will + # go into an infinite loop. + if col_unit.currentColumn == pairing and col_unit.currentUnit != column_changed.currentUnit: + col_unit.currentUnit = column_changed.currentUnit + self.column_changed.emit() + + + def __init__(self, cols: int, options: list[str]): + super().__init__() + + self.cols = cols + self.options = options + self.layout = QHBoxLayout(self) + self.option_widgets: list[ColumnUnit] = [] + for _ in range(cols): + new_widget = ColumnUnit(self.options) + new_widget.column_changed.connect(self.onColumnUpdate) + self.layout.addWidget(new_widget) + self.option_widgets.append(new_widget) + + def setCols(self, new_cols: int): + """Set the amount of columns for the user to edit.""" + + # Decides whether we need to extend the current set of combo boxes, or + # remove some. + if self.cols < new_cols: + for _ in range(new_cols - self.cols): + new_widget = ColumnUnit(self.options) + new_widget.column_changed.connect(self.onColumnUpdate) + self.layout.addWidget(new_widget) + self.option_widgets.append(new_widget) + + self.cols = new_cols + if self.cols > new_cols: + excess_cols = self.cols - new_cols + length = len(self.option_widgets) + excess_combo_boxes = self.option_widgets[length - excess_cols:length] + for box in excess_combo_boxes: + self.layout.removeWidget(box) + box.setParent(None) + self.option_widgets = self.option_widgets[0:length - excess_cols] + self.cols = new_cols + self.column_changed.emit() + + def setColOrder(self, cols: list[str]): + """Sets the series of currently selected columns to be cols, in that + order. If there are not enough column widgets include as many of the + columns in cols as possible. + + """ + try: + for i, col_name in enumerate(cols): + self.option_widgets[i].setCurrentColumn(col_name) + except IndexError: + pass # Can ignore because it means we've run out of widgets. + + def colNames(self) -> list[str]: + """Get a list of all of the currently selected columns.""" + return [widget.currentColumn for widget in self.option_widgets] + + @property + def columns(self) -> list[tuple[str, NamedUnit]]: + return [(widget.currentColumn, widget.currentUnit) for widget in self.option_widgets] + + def replaceOptions(self, new_options: list[str]) -> None: + """Replace options from which the user can choose for each column.""" + self.options = new_options + for widget in self.option_widgets: + widget.replaceOptions(new_options) diff --git a/src/ascii_dialog/column_unit.py b/src/ascii_dialog/column_unit.py new file mode 100644 index 0000000000..fa437109e3 --- /dev/null +++ b/src/ascii_dialog/column_unit.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 + +from PySide6.QtCore import Signal, Slot +from PySide6.QtWidgets import QComboBox, QCompleter, QHBoxLayout, QSizePolicy, QWidget +from PySide6.QtGui import QRegularExpressionValidator +from sasdata.dataset_types import unit_kinds +from sasdata.quantities.units import symbol_lookup, NamedUnit + +from unit_selector import UnitSelector +from default_units import default_units + +def configure_size_policy(combo_box: QComboBox) -> None: + policy = combo_box.sizePolicy() + policy.setHorizontalPolicy(QSizePolicy.Policy.Ignored) + combo_box.setSizePolicy(policy) + +class ColumnUnit(QWidget): + """Widget with 2 combo boxes: one allowing the user to pick a column, and + another to specify the units for that column.""" + def __init__(self, options) -> None: + super().__init__() + self.col_widget = self.createColComboBox(options) + self.unit_widget = self.createUnitComboBox(self.col_widget.currentText()) + self.layout = QHBoxLayout(self) + self.layout.addWidget(self.col_widget) + self.layout.addWidget(self.unit_widget) + self.current_option: str + + column_changed = Signal() + + def createColComboBox(self, options: list[str]) -> QComboBox: + """Create the combo box for specifying the column based on the given + options.""" + new_combo_box = QComboBox() + configure_size_policy(new_combo_box) + for option in options: + new_combo_box.addItem(option) + new_combo_box.setEditable(True) + validator = QRegularExpressionValidator(r"[a-zA-Z0-9]+") + new_combo_box.setValidator(validator) + new_combo_box.currentTextChanged.connect(self.onOptionChange) + return new_combo_box + + def createUnitComboBox(self, selected_option: str) -> QComboBox: + """Create the combo box for specifying the unit for selected_option""" + new_combo_box = QComboBox() + configure_size_policy(new_combo_box) + new_combo_box.setEditable(True) + self.updateUnits(new_combo_box, selected_option) + new_combo_box.currentTextChanged.connect(self.onUnitChange) + return new_combo_box + + def updateUnits(self, unit_box: QComboBox, selected_option: str): + unit_box.clear() + self.current_option = selected_option + # Use the list of preferred units but fallback to the first 5 if there aren't any for this particular column. + unit_options = default_units.get(self.current_option, unit_kinds[selected_option].units) + option_symbols = [unit.symbol for unit in unit_options] + for option in option_symbols[:5]: + unit_box.addItem(option) + unit_box.addItem('Select More') + + + def replaceOptions(self, new_options) -> None: + """Replace the old options for the column with new_options""" + self.col_widget.clear() + self.col_widget.addItems(new_options) + + def setCurrentColumn(self, new_column_value: str) -> None: + """Change the current selected column to new_column_value""" + self.col_widget.setCurrentText(new_column_value) + self.updateUnits(self.unit_widget, new_column_value) + + + @Slot() + def onOptionChange(self): + # If the new option is empty string, its probably because the current + # options have been removed. Can safely ignore this. + self.column_changed.emit() + new_option = self.col_widget.currentText() + if new_option == '': + return + try: + self.updateUnits(self.unit_widget, new_option) + except KeyError: + # Means the units for this column aren't known. This shouldn't be + # the case in the real version so for now we'll just clear the unit + # widget. + self.unit_widget.clear() + + @Slot() + def onUnitChange(self): + new_text = self.unit_widget.currentText() + if new_text == 'Select More': + selector = UnitSelector(unit_kinds[self.col_widget.currentText()].name, False) + selector.exec() + # We need the selection unit in the list of options, or else QT has some dodgy behaviour. + self.unit_widget.insertItem(-1, selector.selected_unit.symbol) + self.unit_widget.setCurrentText(selector.selected_unit.symbol) + # This event could get triggered when the units have just been cleared, and not actually updated. We don't want + # to trigger it in this case. + elif not new_text == '': + self.column_changed.emit() + + @property + def currentColumn(self): + """The currently selected column.""" + return self.col_widget.currentText() + + @property + def currentUnit(self) -> NamedUnit: + """The currently selected unit.""" + current_unit_symbol = self.unit_widget.currentText() + for unit in unit_kinds[self.current_option].units: + if current_unit_symbol == unit.symbol: + return unit + # This error shouldn't really happen so if it does, it indicates there is a bug in the code. + raise ValueError("Current unit doesn't seem to exist") + + @currentUnit.setter + def currentUnit(self, new_value: NamedUnit): + self.unit_widget.setCurrentText(new_value.symbol) diff --git a/src/ascii_dialog/constants.py b/src/ascii_dialog/constants.py new file mode 100644 index 0000000000..bf789cf38c --- /dev/null +++ b/src/ascii_dialog/constants.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python + + +TABLE_MAX_ROWS = 1000 +NOFILE_TEXT = "Click the button below to load a file." diff --git a/src/ascii_dialog/default_units.py b/src/ascii_dialog/default_units.py new file mode 100644 index 0000000000..540654a273 --- /dev/null +++ b/src/ascii_dialog/default_units.py @@ -0,0 +1,12 @@ +# NOTE: This module will probably be a lot more involved once how this is getting into the configuration will be sorted. + +import sasdata.quantities.units as unit + +# Based on the email Jeff sent me./ +default_units = { + 'Q': [unit.per_nanometer, unit.per_angstrom], + # TODO: I think the unit group for scattering intensity may be wrong. Defaulting to nanometers for now but I know + # this isn't right + 'I': [unit.per_nanometer] +} + diff --git a/src/ascii_dialog/dialog.py b/src/ascii_dialog/dialog.py new file mode 100644 index 0000000000..58e8522abd --- /dev/null +++ b/src/ascii_dialog/dialog.py @@ -0,0 +1,479 @@ +from PySide6.QtGui import QColor, QContextMenuEvent, QCursor, Qt +from PySide6.QtWidgets import QAbstractScrollArea, QCheckBox, QComboBox, QFileDialog, QHBoxLayout, QHeaderView, QLabel, \ + QMessageBox, QPushButton, QSpinBox, QTableWidget, QTableWidgetItem, QVBoxLayout, QWidget, QApplication, QDialog +from PySide6.QtCore import QModelIndex, QPoint, Slot +from selection_menu import SelectionMenu +from warning_label import WarningLabel +from col_editor import ColEditor +from row_status_widget import RowStatusWidget +from guess import guess_column_count, guess_columns, guess_starting_position +from os import path +from sasdata.dataset_types import DatasetType, dataset_types, one_dim, two_dim, sesans +from sasdata.temp_ascii_reader import load_data, AsciiReaderParams, split_line +from metadata_filename_gui.metadata_filename_dialog import MetadataFilenameDialog +from metadata_filename_gui.metadata_tree_data import initial_metadata_dict +from sasdata.ascii_reader_metadata import AsciiReaderMetadata +from constants import TABLE_MAX_ROWS, NOFILE_TEXT +import re + +dataset_dictionary = dict([(dataset.name, dataset) for dataset in [one_dim, two_dim, sesans]]) + +class AsciiDialog(QDialog): + """A dialog window allowing the user to adjust various properties regarding + how an ASCII file should be interpreted. This widget allows the user to + visualise what the data will look like with the parameter the user has + selected. + + """ + def __init__(self): + super().__init__() + + self.files: dict[str, list[str]] = {} + self.files_full_path: dict[str, str] = {} + self.files_is_included: dict[str, list[bool]] = {} + # This is useful for whenever the user wants to reopen the metadata editor. + self.internal_metadata: AsciiReaderMetadata = AsciiReaderMetadata() + self.current_filename: str | None = None + + self.seperators: dict[str, bool] = { + 'Comma': True, + 'Whitespace': True, + 'Tab': True + } + + self.setWindowTitle('ASCII File Reader') + + # Filename, unload button, and edit metadata button. + + self.filename_unload_layout = QHBoxLayout() + self.filename_label = QLabel(NOFILE_TEXT) + self.unloadButton = QPushButton("Unload") + self.unloadButton.setDisabled(True) + self.editMetadataButton = QPushButton("Edit Metadata") + self.editMetadataButton.setDisabled(True) + self.editMetadataButton.clicked.connect(self.editMetadata) + self.unloadButton.clicked.connect(self.unload) + self.filename_unload_layout.addWidget(self.filename_label) + self.filename_unload_layout.addWidget(self.unloadButton) + self.filename_unload_layout.addWidget(self.editMetadataButton) + + # Filename chooser + self.filename_chooser = QComboBox() + self.filename_chooser.currentTextChanged.connect(self.updateCurrentFile) + + self.load_button = QPushButton("Load File") + self.load_button.clicked.connect(self.load_file) + + ## Dataset type selection + self.dataset_layout = QHBoxLayout() + self.dataset_label = QLabel("Dataset Type") + self.dataset_combobox = QComboBox() + for name in dataset_types: + self.dataset_combobox.addItem(name) + self.dataset_layout.addWidget(self.dataset_label) + self.dataset_layout.addWidget(self.dataset_combobox) + + ## Seperator + self.sep_layout = QHBoxLayout() + + self.sep_widgets: list[QWidget] = [] + self.sep_label = QLabel('Seperators:') + self.sep_layout.addWidget(self.sep_label) + for seperator_name, value in self.seperators.items(): + check_box = QCheckBox(seperator_name) + check_box.setChecked(value) + check_box.clicked.connect(self.seperatorToggle) + self.sep_widgets.append(check_box) + self.sep_layout.addWidget(check_box) + + ## Starting Line + self.startline_layout = QHBoxLayout() + self.startline_label = QLabel('Starting Line') + self.startline_entry = QSpinBox() + self.startline_entry.setMinimum(1) + self.startline_entry.valueChanged.connect(self.updateStartpos) + self.startline_layout.addWidget(self.startline_label) + self.startline_layout.addWidget(self.startline_entry) + + ## Column Count + self.colcount_layout = QHBoxLayout() + self.colcount_label = QLabel('Number of Columns') + self.colcount_entry = QSpinBox() + self.colcount_entry.setMinimum(1) + self.colcount_entry.valueChanged.connect(self.updateColcount) + self.colcount_layout.addWidget(self.colcount_label) + self.colcount_layout.addWidget(self.colcount_entry) + + ## Column Editor + options = self.datasetOptions() + self.col_editor = ColEditor(self.colcount_entry.value(), options) + self.dataset_combobox.currentTextChanged.connect(self.changeDatasetType) + self.col_editor.column_changed.connect(self.updateColumn) + + ## Data Table + + self.table = QTableWidget() + self.table.show() + # Make the table readonly + self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers) + # The table's width will always resize to fit the amount of space it has. + self.table.setSizeAdjustPolicy(QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents) + # Add the context menu + self.table.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) + self.table.customContextMenuRequested.connect(self.showContextMenu) + + # Warning Label + self.warning_label: WarningLabel = WarningLabel(self.requiredMissing(), self.duplicateColumns()) + + # Done button + # TODO: Not entirely sure what to call/label this. Just going with 'done' for now. + + self.done_button = QPushButton('Done') + self.done_button.clicked.connect(self.onDoneButton) + + self.layout = QVBoxLayout(self) + + self.layout.addLayout(self.filename_unload_layout) + self.layout.addWidget(self.filename_chooser) + self.layout.addWidget(self.load_button) + self.layout.addLayout(self.dataset_layout) + self.layout.addLayout(self.sep_layout) + self.layout.addLayout(self.startline_layout) + self.layout.addLayout(self.colcount_layout) + self.layout.addWidget(self.col_editor) + self.layout.addWidget(self.table) + self.layout.addWidget(self.warning_label) + self.layout.addWidget(self.done_button) + + @property + def starting_pos(self) -> int: + return self.startline_entry.value() - 1 + + @starting_pos.setter + def starting_pos(self, value: int): + self.startline_entry.setValue(value + 1) + + @property + def raw_csv(self) -> list[str] | None: + if self.current_filename is None: + return None + return self.files[self.current_filename] + + @property + def rows_is_included(self) -> list[bool] | None: + if self.current_filename is None: + return None + return self.files_is_included[self.current_filename] + + @property + def excluded_lines(self) -> set[int]: + return set([i for i, included in enumerate(self.rows_is_included) if not included]) + + def splitLine(self, line: str) -> list[str]: + """Split a line in a CSV file based on which seperators the user has + selected on the widget. + + """ + return split_line(self.seperators, line) + + def attemptGuesses(self) -> None: + """Attempt to guess various parameters of the data to provide some + default values. Uses the guess.py module + + """ + split_csv = [self.splitLine(line.strip()) for line in self.raw_csv] + + # TODO: I'm not sure if there is any point in holding this initial value. Can possibly be refactored. + self.initial_starting_pos = guess_starting_position(split_csv) + + guessed_colcount = guess_column_count(split_csv, self.initial_starting_pos) + self.col_editor.setCols(guessed_colcount) + + columns = guess_columns(guessed_colcount, self.currentDatasetType()) + self.col_editor.setColOrder(columns) + self.colcount_entry.setValue(guessed_colcount) + self.starting_pos = self.initial_starting_pos + + def fillTable(self) -> None: + """Write the data to the table based on the parameters the user has + selected. + + """ + + # Don't try to fill the table if there's no data. + if self.raw_csv is None: + return + + self.table.clear() + + col_count = self.colcount_entry.value() + + self.table.setRowCount(min(len(self.raw_csv), TABLE_MAX_ROWS + 1)) + self.table.setColumnCount(col_count + 1) + self.table.setHorizontalHeaderLabels(["Included"] + self.col_editor.colNames()) + self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Stretch) + + # Now fill the table with data + for i, row in enumerate(self.raw_csv): + if i == TABLE_MAX_ROWS: + # Fill with elipsis to indicate there is more data. + for j in range(len(row_split)): + elipsis_item = QTableWidgetItem("...") + elipsis_item.setTextAlignment(Qt.AlignmentFlag.AlignCenter) + self.table.setItem(i, j, elipsis_item) + break + + if i < len(self.rows_is_included): + initial_state = self.rows_is_included[i] + else: + initial_state = True + self.rows_is_included.append(initial_state) + if i >= self.starting_pos: + row_status = RowStatusWidget(initial_state, i) + row_status.status_changed.connect(self.updateRowStatus) + self.table.setCellWidget(i, 0, row_status) + row_split = self.splitLine(row) + for j, col_value in enumerate(row_split): + if j >= col_count: + continue # Ignore rows that have extra columns. + item = QTableWidgetItem(col_value) + self.table.setItem(i, j + 1, item) + self.setRowTypesetting(i, self.rows_is_included[i]) + + self.table.show() + + def currentDatasetType(self) -> DatasetType: + """Get the dataset type that the user has currently selected.""" + return dataset_dictionary[self.dataset_combobox.currentText()] + + def setRowTypesetting(self, row: int, item_checked: bool) -> None: + """Set the typesetting for the given role depending on whether it is to + be included in the data being loaded, or not. + + """ + for column in range(1, self.table.columnCount() + 1): + item = self.table.item(row, column) + if item is None: + continue + item_font = item.font() + if not item_checked or row < self.starting_pos: + item.setForeground(QColor.fromString('grey')) + item_font.setStrikeOut(True) + else: + item.setForeground(QColor.fromString('black')) + item_font.setStrikeOut(False) + item.setFont(item_font) + + def updateWarningLabel(self): + required_missing = self.requiredMissing() + duplicates = self.duplicateColumns() + if self.raw_csv is None: + # We don't have any actual data yet so we're just updating the warning based on the column. + self.warning_label.update_warning(required_missing, duplicates) + else: + self.warning_label.update_warning(required_missing, duplicates, [self.splitLine(line) for line in self.raw_csv], self.rows_is_included, self.starting_pos) + + @Slot() + def load_file(self) -> None: + """Open the file loading dialog, and load the file the user selects.""" + filenames, result = QFileDialog.getOpenFileNames(self) + # Happens when the user cancels without selecting a file. There isn't a + # file to load in this case. + if result == '': + return + for filename in filenames: + + basename = path.basename(filename) + self.filename_label.setText(basename) + + try: + with open(filename) as file: + file_csv = file.readlines() + file_csv = [line.strip() for line in file_csv] + # TODO: This assumes that no two files will be loaded with the same + # name. This might not be a reasonable assumption. + self.files[basename] = file_csv + self.files_full_path[basename] = filename + # Reset checkboxes + self.files_is_included[basename] = [] + if len(self.files) == 1: + # Default behaviour is going to be to set this to the first file we load. This seems sensible but + # may provoke further discussion. + self.current_filename = basename + # This will trigger the update current file event which will cause + # the table to be drawn. + self.internal_metadata.init_separator(basename) + self.filename_chooser.addItem(basename) + self.filename_chooser.setCurrentText(basename) + self.internal_metadata.add_file(basename) + + except OSError: + QMessageBox.critical(self, 'File Read Error', f'There was an error reading {basename}') + except UnicodeDecodeError: + QMessageBox.critical(self, 'File Read Error', f"""There was an error decoding {basename}. +This could potentially be because the file {basename} an ASCII format.""") + # Attempt guesses on the first file that was loaded. + self.attemptGuesses() + + @Slot() + def unload(self) -> None: + del self.files[self.current_filename] + self.filename_chooser.removeItem(self.filename_chooser.currentIndex()) + # Filename chooser should now revert back to a different file. + self.updateCurrentFile() + + @Slot() + def updateColcount(self) -> None: + """Triggered when the amount of columns the user has selected has + changed. + + """ + self.col_editor.setCols(self.colcount_entry.value()) + self.fillTable() + self.updateWarningLabel() + + @Slot() + def updateStartpos(self) -> None: + """Triggered when the starting position of the data has changed.""" + self.fillTable() + self.updateWarningLabel() + + @Slot() + def updateSeperator(self) -> None: + """Changed when the user modifies the set of seperators being used.""" + self.fillTable() + self.updateWarningLabel() + + @Slot() + def updateColumn(self) -> None: + """Triggered when any of the columns has been changed.""" + self.fillTable() + self.updateWarningLabel() + + @Slot() + def updateCurrentFile(self) -> None: + """Triggered when the current file (choosen from the file chooser + ComboBox) changes. + + """ + self.current_filename = self.filename_chooser.currentText() + self.filename_label.setText(self.current_filename) + if self.current_filename == '': + self.table.clear() + self.filename_label.setText(NOFILE_TEXT) + self.table.setDisabled(True) + self.unloadButton.setDisabled(True) + self.editMetadataButton.setDisabled(True) + # Set this to None because other methods are expecting this. + self.current_filename = None + else: + self.table.setDisabled(False) + self.unloadButton.setDisabled(False) + self.editMetadataButton.setDisabled(False) + self.fillTable() + self.updateWarningLabel() + + @Slot() + def seperatorToggle(self) -> None: + """Triggered when one of the seperator check boxes has been toggled.""" + check_box = self.sender() + self.seperators[check_box.text()] = check_box.isChecked() + self.fillTable() + self.updateWarningLabel() + + @Slot() + def changeDatasetType(self) -> None: + """Triggered when the selected dataset type has changed.""" + options = self.datasetOptions() + self.col_editor.replaceOptions(options) + + # Update columns as they'll be different now. + columns = guess_columns(self.colcount_entry.value(), self.currentDatasetType()) + self.col_editor.setColOrder(columns) + + @Slot() + def updateRowStatus(self, row: int) -> None: + """Triggered when the status of row has changed.""" + new_status = self.table.cellWidget(row, 0).isChecked() + self.rows_is_included[row] = new_status + self.setRowTypesetting(row, new_status) + + @Slot() + def showContextMenu(self, point: QPoint) -> None: + """Show the context menu for the table.""" + context_menu = SelectionMenu(self) + context_menu.select_all_event.connect(self.selectItems) + context_menu.deselect_all_event.connect(self.deselectItems) + context_menu.exec(QCursor.pos()) + + def changeInclusion(self, indexes: list[QModelIndex], new_value: bool): + for index in indexes: + # This will happen if the user has selected a point which exists before the starting line. To prevent an + # error, this code will skip that position. + row = index.row() + if row < self.starting_pos: + continue + self.table.cellWidget(row, 0).setChecked(new_value) + self.updateRowStatus(row) + + @Slot() + def selectItems(self) -> None: + """Include all of the items that have been selected in the table.""" + self.changeInclusion(self.table.selectedIndexes(), True) + self.updateWarningLabel() + + @Slot() + def deselectItems(self) -> None: + """Don't include all of the items that have been selected in the table.""" + self.changeInclusion(self.table.selectedIndexes(), False) + self.updateWarningLabel() + + def requiredMissing(self) -> list[str]: + """Returns all the columns that are required by the dataset type but + have not currently been selected. + + """ + dataset = self.currentDatasetType() + missing_columns = [col for col in dataset.required if col not in self.col_editor.colNames()] + return missing_columns + + def duplicateColumns(self) -> set[str]: + """Returns all of the columns which have been selected multiple times.""" + col_names = self.col_editor.colNames() + return set([col for col in col_names if not col == '' and col_names.count(col) > 1]) + + def datasetOptions(self) -> list[str]: + current_dataset_type = self.currentDatasetType() + return current_dataset_type.required + current_dataset_type.optional + [''] + + def onDoneButton(self): + params = AsciiReaderParams( + list(self.files_full_path.values()), + self.starting_pos, + self.col_editor.columns, + self.excluded_lines, + self.seperators, + self.internal_metadata + ) + self.params = params + self.accept() + + def editMetadata(self): + dialog = MetadataFilenameDialog(self.current_filename, self.internal_metadata) + status = dialog.exec() + if status == 1: + self.internal_metadata = dialog.internal_metadata + + +if __name__ == "__main__": + app = QApplication([]) + + dialog = AsciiDialog() + status = dialog.exec() + # 1 means the dialog was accepted. + if status == 1: + loaded = load_data(dialog.params) + for datum in loaded: + print(datum.summary()) + + exit() diff --git a/src/ascii_dialog/guess.py b/src/ascii_dialog/guess.py new file mode 100644 index 0000000000..50fad24658 --- /dev/null +++ b/src/ascii_dialog/guess.py @@ -0,0 +1,35 @@ +from sasdata.dataset_types import DatasetType + +def guess_column_count(split_csv: list[list[str]], starting_pos: int) -> int: + """Guess the amount of columns present in the data.""" + return len(split_csv[starting_pos]) + +def guess_columns(col_count: int, dataset_type: DatasetType) -> list[str]: + """Based on the amount of columns specified in col_count, try to find a set + of columns that best matchs the dataset_type. + + """ + # Ideally we want an exact match but if the ordering is bigger than the col + # count then we can accept that as well. + for order_list in dataset_type.expected_orders: + if len(order_list) >= col_count: + return order_list + + return dataset_type.expected_orders[-1] + +def guess_starting_position(split_csv: list[list[str]]) -> int: + """Try to look for a line where the first item in the row can be converted + to a number. If such a line doesn't exist, try to look for a line where the + first item in the row can be converted to a number. If such a line doesn't + exist, then just return 0 as the starting position. + + """ + for i, row in enumerate(split_csv): + all_nums = True + for column in row: + if not column.replace('.', '').replace('-', '').isdigit(): + all_nums = False + break + if all_nums: + return i + return 0 diff --git a/src/ascii_dialog/row_status_widget.py b/src/ascii_dialog/row_status_widget.py new file mode 100644 index 0000000000..eade62b67f --- /dev/null +++ b/src/ascii_dialog/row_status_widget.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +from PySide6.QtCore import Signal, Slot +from PySide6.QtWidgets import QCheckBox + + +class RowStatusWidget(QCheckBox): + """Widget to toggle whether the row is to be included as part of the data.""" + def __init__(self, initial_value: bool, row: int): + super().__init__() + self.row = row + self.setChecked(initial_value) + self.updateLabel() + self.stateChanged.connect(self.onStateChange) + + status_changed = Signal(int) + def updateLabel(self): + """Update the label of the check box depending on whether it is checked, + or not.""" + if self.isChecked(): + self.setText('Included') + else: + self.setText('Not Included') + + + @Slot() + def onStateChange(self): + self.updateLabel() + self.status_changed.emit(self.row) diff --git a/src/ascii_dialog/selection_menu.py b/src/ascii_dialog/selection_menu.py new file mode 100644 index 0000000000..ef95ac04d5 --- /dev/null +++ b/src/ascii_dialog/selection_menu.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 + + +from PySide6.QtCore import Signal +from PySide6.QtGui import QAction +from PySide6.QtWidgets import QMenu, QWidget + +class SelectionMenu(QMenu): + select_all_event = Signal() + deselect_all_event = Signal() + + def __init__(self, parent: QWidget): + super().__init__(parent) + + select_all = QAction("Select All", parent) + select_all.triggered.connect(self.select_all_event) + + deselect_all = QAction("Deselect All", parent) + deselect_all.triggered.connect(self.deselect_all_event) + + self.addAction(select_all) + self.addAction(deselect_all) diff --git a/src/ascii_dialog/unit_list_widget.py b/src/ascii_dialog/unit_list_widget.py new file mode 100644 index 0000000000..1bd64c70bf --- /dev/null +++ b/src/ascii_dialog/unit_list_widget.py @@ -0,0 +1,24 @@ +#!/usr/bin/env python3 + +from PySide6.QtWidgets import QListWidget, QListWidgetItem +from sasdata.quantities.units import NamedUnit + + +class UnitListWidget(QListWidget): + def reprUnit(self, unit: NamedUnit) -> str: + return f"{unit.symbol} ({unit.name})" + + def populateList(self, units: list[NamedUnit]) -> None: + self.clear() + self.units = units + for unit in units: + item = QListWidgetItem(self.reprUnit(unit)) + self.addItem(item) + + @property + def selectedUnit(self) -> NamedUnit | None: + return self.units[self.currentRow()] + + def __init__(self): + super().__init__() + self.units: list[NamedUnit] = [] diff --git a/src/ascii_dialog/unit_preference_line.py b/src/ascii_dialog/unit_preference_line.py new file mode 100644 index 0000000000..5e00d0f571 --- /dev/null +++ b/src/ascii_dialog/unit_preference_line.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +from PySide6.QtCore import Slot +from PySide6.QtWidgets import QHBoxLayout, QLabel, QPushButton, QWidget +from sasdata.quantities.units import NamedUnit, UnitGroup + +from unit_selector import UnitSelector + +class UnitPreferenceLine(QWidget): + def __init__(self, column_name: str, initial_unit: NamedUnit, group: UnitGroup): + super().__init__() + + self.group = group + self.current_unit = initial_unit + + self.column_label = QLabel(column_name) + self.unit_button = QPushButton(initial_unit.symbol) + self.unit_button.clicked.connect(self.onUnitPress) + + self.layout = QHBoxLayout(self) + self.layout.addWidget(self.column_label) + self.layout.addWidget(self.unit_button) + + @Slot() + def onUnitPress(self): + picker = UnitSelector(self.group.name, False) + picker.exec() + self.current_unit = picker.selected_unit + self.unit_button.setText(self.current_unit.symbol) diff --git a/src/ascii_dialog/unit_preferences.py b/src/ascii_dialog/unit_preferences.py new file mode 100644 index 0000000000..fb49fc8879 --- /dev/null +++ b/src/ascii_dialog/unit_preferences.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +from PySide6.QtGui import Qt +from PySide6.QtWidgets import QApplication, QScrollArea, QVBoxLayout, QWidget +from sasdata.quantities.units import NamedUnit +from sasdata.dataset_types import unit_kinds +from unit_preference_line import UnitPreferenceLine +import random + +class UnitPreferences(QWidget): + def __init__(self): + super().__init__() + + # TODO: Presumably this will be loaded from some config from somewhere. + # For now just fill it with some placeholder values. + column_names = unit_kinds.keys() + self.columns: dict[str, NamedUnit] = {} + for name in column_names: + self.columns[name] = random.choice(unit_kinds[name].units) + + self.layout = QVBoxLayout(self) + preference_lines = QWidget() + scroll_area = QScrollArea() + scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + scroll_layout = QVBoxLayout(preference_lines) + for column_name, unit in self.columns.items(): + line = UnitPreferenceLine(column_name, unit, unit_kinds[column_name]) + scroll_layout.addWidget(line) + + scroll_area.setWidget(preference_lines) + self.layout.addWidget(scroll_area) + + +if __name__ == "__main__": + app = QApplication([]) + + widget = UnitPreferences() + widget.show() + + exit(app.exec()) diff --git a/src/ascii_dialog/unit_selector.py b/src/ascii_dialog/unit_selector.py new file mode 100644 index 0000000000..07ffc9e5c6 --- /dev/null +++ b/src/ascii_dialog/unit_selector.py @@ -0,0 +1,79 @@ +from PySide6.QtCore import Slot +from PySide6.QtWidgets import QApplication, QComboBox, QDialog, QLineEdit, QPushButton, QVBoxLayout +from sasdata.quantities.units import NamedUnit, UnitGroup, unit_group_names, unit_groups + +from unit_list_widget import UnitListWidget + +all_unit_groups = list(unit_groups.values()) + +class UnitSelector(QDialog): + def currentUnitGroup(self) -> UnitGroup: + index = self.unit_type_selector.currentIndex() + return all_unit_groups[index] + + @property + def selected_unit(self) -> NamedUnit | None: + return self.unit_list_widget.selectedUnit + + @Slot() + def onSearchChanged(self): + search_input = self.search_box.text() + current_group = self.currentUnitGroup() + units = current_group.units + if search_input != '': + units = [unit for unit in units if search_input.lower() in unit.name] + self.unit_list_widget.populateList(units) + + + @Slot() + def unitGroupChanged(self): + new_group = self.currentUnitGroup() + self.search_box.setText('') + self.unit_list_widget.populateList(new_group.units) + + @Slot() + def selectUnit(self): + self.accept() + + @Slot() + def selectionChanged(self): + self.select_button.setDisabled(False) + + def __init__(self, default_group='length', allow_group_edit=True): + super().__init__() + + self.unit_type_selector = QComboBox() + self.unit_type_selector.addItems(unit_group_names) + self.unit_type_selector.setCurrentText(default_group) + if not allow_group_edit: + self.unit_type_selector.setDisabled(True) + self.unit_type_selector.currentTextChanged.connect(self.unitGroupChanged) + + self.search_box = QLineEdit() + self.search_box.textChanged.connect(self.onSearchChanged) + self.search_box.setPlaceholderText('Search for a unit...') + + self.unit_list_widget = UnitListWidget() + # TODO: Are they all named units? + self.unit_list_widget.populateList(self.currentUnitGroup().units) + self.unit_list_widget.itemSelectionChanged.connect(self.selectionChanged) + self.unit_list_widget.itemDoubleClicked.connect(self.selectUnit) + + self.select_button = QPushButton('Select Unit') + self.select_button.pressed.connect(self.selectUnit) + self.select_button.setDisabled(True) + + self.layout = QVBoxLayout(self) + self.layout.addWidget(self.unit_type_selector) + self.layout.addWidget(self.search_box) + self.layout.addWidget(self.unit_list_widget) + self.layout.addWidget(self.select_button) + +if __name__ == "__main__": + app = QApplication([]) + + widget = UnitSelector() + widget.exec() + print(widget.selected_unit) + + exit() diff --git a/src/ascii_dialog/warning_label.py b/src/ascii_dialog/warning_label.py new file mode 100644 index 0000000000..4e76352978 --- /dev/null +++ b/src/ascii_dialog/warning_label.py @@ -0,0 +1,54 @@ +from PySide6.QtWidgets import QLabel +from constants import TABLE_MAX_ROWS + + +class WarningLabel(QLabel): + """Widget to display an appropriate warning message based on whether there + exists columns that are missing, or there are columns that are duplicated. + + """ + def setFontRed(self): + self.setStyleSheet("QLabel { color: red}") + + def setFontOrange(self): + self.setStyleSheet("QLabel { color: orange}") + + def setFontNormal(self): + self.setStyleSheet('') + + def update_warning(self, missing_columns: list[str], duplicate_columns: list[str], lines: list[list[str]] | None = None, rows_is_included: list[bool] | None = None, starting_pos: int = 0): + """Determine, and set the appropriate warning messages given how many + columns are missing, and how many columns are duplicated.""" + unparsable = 0 + if lines is not None and rows_is_included is not None: + # FIXME: I feel like I am repeating a lot of logic from the table filling. Is there a way I can abstract + # this? + for i, line in enumerate(lines): + # Right now, rows_is_included only includes a limited number of rows as there is a maximum that can be + # shown in the table without it being really laggy. We're just going to assume the lines after it should + # be included. + if (i >= TABLE_MAX_ROWS or rows_is_included[i]) and i >= starting_pos: + # TODO: Is there really no builtin function for this? I don't like using try/except like this. + try: + for item in line: + _ = float(item) + except: + unparsable += 1 + + if len(missing_columns) != 0: + self.setText(f'The following columns are missing: {missing_columns}') + self.setFontRed() + elif len(duplicate_columns) > 0: + self.setText(f'There are duplicate columns.') + self.setFontRed() + elif unparsable > 0: + # FIXME: This error message could perhaps be a bit clearer. + self.setText(f'{unparsable} lines cannot be parsed. They will be ignored.') + self.setFontOrange() + else: + self.setText('All is fine') # TODO: Probably want to find a more appropriate message. + self.setFontNormal() + + def __init__(self, initial_missing_columns, initial_duplicate_classes): + super().__init__() + self.update_warning(initial_missing_columns, initial_duplicate_classes) diff --git a/src/metadata_filename_gui/metadata_component_selector.py b/src/metadata_filename_gui/metadata_component_selector.py new file mode 100644 index 0000000000..e334baccb6 --- /dev/null +++ b/src/metadata_filename_gui/metadata_component_selector.py @@ -0,0 +1,57 @@ +from PySide6.QtWidgets import QWidget, QPushButton, QHBoxLayout +from PySide6.QtCore import Signal, Qt, Slot + +from sasdata.ascii_reader_metadata import AsciiReaderMetadata + +class MetadataComponentSelector(QWidget): + # Creating a separate signal for this because the custom button may be destroyed/recreated whenever the options are + # redrawn. + + custom_button_pressed = Signal(Qt.MouseButton()) + + def __init__(self, category: str, metadatum: str, filename: str, internal_metadata: AsciiReaderMetadata): + super().__init__() + self.options: list[str] + self.option_buttons: list[QPushButton] + self.layout = QHBoxLayout(self) + self.internal_metadata = internal_metadata + self.metadatum = metadatum + self.category = category + self.filename = filename + + def clear_options(self): + for i in reversed(range(self.layout.count() - 1)): + self.layout.takeAt(i).widget().deleteLater() + + def draw_options(self, new_options: list[str], selected_option: str | None): + self.clear_options() + self.options = new_options + self.option_buttons = [] + for option in self.options: + option_button = QPushButton(option) + option_button.setCheckable(True) + option_button.clicked.connect(self.selection_changed) + option_button.setChecked(option == selected_option) + self.layout.addWidget(option_button) + self.option_buttons.append(option_button) + # This final button is to convert to use custom entry instead of this. + self.custom_entry_button = QPushButton('Custom') + # self.custom_entry_button.clicked.connect(self.custom_button_pressed) + self.custom_entry_button.clicked.connect(self.handle_custom_button) + self.layout.addWidget(self.custom_entry_button) + + def handle_custom_button(self): + self.custom_button_pressed.emit() + + def selection_changed(self): + selected_button: QPushButton = self.sender() + button_index = -1 + for i, button in enumerate(self.option_buttons): + if button != selected_button: + button.setChecked(False) + else: + button_index = i + if selected_button.isChecked(): + self.internal_metadata.update_metadata(self.category, self.metadatum, self.filename, button_index) + else: + self.internal_metadata.clear_metadata(self.category, self.metadatum, self.filename) diff --git a/src/metadata_filename_gui/metadata_custom_selector.py b/src/metadata_filename_gui/metadata_custom_selector.py new file mode 100644 index 0000000000..a33d9a5c1f --- /dev/null +++ b/src/metadata_filename_gui/metadata_custom_selector.py @@ -0,0 +1,28 @@ +from PySide6.QtWidgets import QWidget, QLineEdit, QPushButton, QHBoxLayout + +from sasdata.ascii_reader_metadata import AsciiReaderMetadata + +class MetadataCustomSelector(QWidget): + def __init__(self, category:str, metadatum: str, internal_metadata: AsciiReaderMetadata, filename: str): + super().__init__() + self.internal_metadata = internal_metadata + self.metadatum = metadatum + self.category = category + self.filename = filename + + prexisting_value = self.internal_metadata.get_metadata(category, metadatum, filename) + initial_value = prexisting_value if prexisting_value is not None else '' + self.entry_box = QLineEdit(initial_value) + self.entry_box.textChanged.connect(self.selection_changed) + self.from_filename_button = QPushButton('From Filename') + + self.layout = QHBoxLayout(self) + self.layout.addWidget(self.entry_box) + self.layout.addWidget(self.from_filename_button) + + def selection_changed(self): + new_value = self.entry_box.text() + if new_value != '': + self.internal_metadata.update_metadata(self.category, self.metadatum, self.filename, new_value) + else: + self.internal_metadata.clear_metadata(self.category, self.metadatum, self.filename) diff --git a/src/metadata_filename_gui/metadata_filename_dialog.py b/src/metadata_filename_gui/metadata_filename_dialog.py new file mode 100644 index 0000000000..25fe439169 --- /dev/null +++ b/src/metadata_filename_gui/metadata_filename_dialog.py @@ -0,0 +1,107 @@ +from PySide6.QtWidgets import QBoxLayout, QButtonGroup, QRadioButton, QWidget, QApplication, QVBoxLayout, QLineEdit, QHBoxLayout, QLabel, QDialog, QPushButton +from metadata_filename_gui.metadata_tree_widget import MetadataTreeWidget +from sasdata.ascii_reader_metadata import AsciiReaderMetadata, CASING_REGEX +from sys import argv +import re + +def build_font(text: str, classname: str = '') -> str: + match classname: + case 'token': + return f"{text}" + case 'separator': + return f"{text}" + case _: + return text + return f'{text}' + +class MetadataFilenameDialog(QDialog): + def __init__(self, filename: str, initial_metadata: AsciiReaderMetadata): + super().__init__() + + # TODO: Will probably change this default later (or a more sophisticated way of getting this default from the + # filename.) + initial_separator_text = initial_metadata.filename_separator[filename] + + self.setWindowTitle('Metadata') + + self.filename = filename + # Key is the metadatum, value is the component selected for it. + self.internal_metadata = initial_metadata + + self.filename_line_label = QLabel() + self.separate_on_group = QButtonGroup() + self.character_radio = QRadioButton("Character") + self.separate_on_group.addButton(self.character_radio) + self.casing_radio = QRadioButton("Casing") + self.separate_on_group.addButton(self.casing_radio) + # Right now, we're going to assume we're separating by character but we need to detect this later. + self.character_radio.setChecked(True) + self.separate_on_layout = QHBoxLayout() + self.separate_on_group.buttonToggled.connect(self.update_filename_separation) + self.separate_on_layout.addWidget(self.filename_line_label) + self.separate_on_layout.addWidget(self.character_radio) + self.separate_on_layout.addWidget(self.casing_radio) + + if not any([char.isupper() for char in self.filename]): + self.casing_radio.setDisabled(True) + + self.seperator_chars_label = QLabel('Seperators') + self.separator_chars = QLineEdit(initial_separator_text) + self.separator_chars.textChanged.connect(self.update_filename_separation) + + self.filename_separator_layout = QHBoxLayout() + self.filename_separator_layout.addWidget(self.seperator_chars_label) + self.filename_separator_layout.addWidget(self.separator_chars) + + self.metadata_tree = MetadataTreeWidget(self.internal_metadata) + + # Have to update this now because it relies on the value of the separator, and tree. + self.update_filename_separation() + + self.save_button = QPushButton('Save') + self.save_button.clicked.connect(self.on_save) + + self.layout = QVBoxLayout(self) + self.layout.addLayout(self.separate_on_layout) + self.layout.addLayout(self.filename_separator_layout) + self.layout.addWidget(self.metadata_tree) + self.layout.addWidget(self.save_button) + + def formatted_filename(self) -> str: + sep_str = self.separator_chars.text() + if sep_str == '' or self.casing_radio.isChecked(): + return f'{self.filename}' + # TODO: Won't escape characters; I'll handle that later. + separated = self.internal_metadata.filename_components(self.filename, False, True) + font_elements = '' + for i, token in enumerate(separated): + classname = 'token' if i % 2 == 0 else 'separator' + font_elements += build_font(token, classname) + return font_elements + + def update_filename_separation(self): + if self.casing_radio.isChecked(): + self.separator_chars.setDisabled(True) + else: + self.separator_chars.setDisabled(False) + self.internal_metadata.filename_separator[self.filename] = self.separator_chars.text() if self.character_radio.isChecked() else True + self.internal_metadata.purge_unreachable(self.filename) + self.filename_line_label.setText(f'Filename: {self.formatted_filename()}') + self.metadata_tree.draw_tree(self.filename) + + def on_save(self): + self.accept() + # Don't really need to do anything else. Anyone using this dialog can access the component_metadata dict. + + + +if __name__ == "__main__": + app = QApplication([]) + if len(argv) < 2: + filename = input('Input filename to test: ') + else: + filename = argv[1] + dialog = MetadataFilenameDialog(filename) + status = dialog.exec() + if status == 1: + print(dialog.component_metadata) diff --git a/src/metadata_filename_gui/metadata_selector.py b/src/metadata_filename_gui/metadata_selector.py new file mode 100644 index 0000000000..be54c56b22 --- /dev/null +++ b/src/metadata_filename_gui/metadata_selector.py @@ -0,0 +1,47 @@ +from PySide6.QtWidgets import QWidget, QHBoxLayout +from sasdata.ascii_reader_metadata import AsciiReaderMetadata +from metadata_filename_gui.metadata_component_selector import MetadataComponentSelector +from metadata_filename_gui.metadata_custom_selector import MetadataCustomSelector + +class MetadataSelector(QWidget): + def __init__(self, category: str, metadatum: str, metadata: AsciiReaderMetadata, filename: str): + super().__init__() + self.category = category + self.metadatum = metadatum + self.metadata: AsciiReaderMetadata = metadata + self.filename = filename + self.options = self.metadata.filename_components(filename) + current_option = self.metadata.get_metadata(self.category, metadatum, filename) + if current_option is None or current_option in self.options: + self.selector_widget = self.new_component_selector() + else: + self.selector_widget = self.new_custom_selector() + + # I can't seem to find any layout that just has one widget in so this will do for now. + self.layout = QHBoxLayout(self) + self.layout.addWidget(self.selector_widget) + + def new_component_selector(self) -> MetadataComponentSelector: + new_selector = MetadataComponentSelector(self.category, self.metadatum, self.filename, self.metadata) + new_selector.custom_button_pressed.connect(self.handle_selector_change) + new_selector.draw_options(self.options, self.metadata.get_metadata(self.category, self.metadatum, self.filename)) + return new_selector + + def new_custom_selector(self) -> MetadataCustomSelector: + new_selector = MetadataCustomSelector(self.category, self.metadatum, self.metadata, self.filename) + new_selector.from_filename_button.clicked.connect(self.handle_selector_change) + return new_selector + + def handle_selector_change(self): + # Need to keep this for when we delete it. + if isinstance(self.selector_widget, MetadataComponentSelector): + # TODO: Will eventually have args + new_widget = self.new_custom_selector() + elif isinstance(self.selector_widget, MetadataCustomSelector): + new_widget = self.new_component_selector() + else: + # Shouldn't happen as selector widget should be either of the above. + return + self.layout.replaceWidget(self.selector_widget, new_widget) + self.selector_widget.deleteLater() + self.selector_widget = new_widget diff --git a/src/metadata_filename_gui/metadata_tree_data.py b/src/metadata_filename_gui/metadata_tree_data.py new file mode 100644 index 0000000000..c2a5690f70 --- /dev/null +++ b/src/metadata_filename_gui/metadata_tree_data.py @@ -0,0 +1,15 @@ +#!/usr/bin/env python + + +metadata = { + 'source': ['name', 'radiation', 'type', 'probe_particle', 'beam_size_name', 'beam_size', 'beam_shape', 'wavelength', 'wavelength_min', 'wavelength_max', 'wavelength_spread'], + 'detector': ['name', 'distance', 'offset', 'orientation', 'beam_center', 'pixel_size', 'slit_length'], + 'aperture': ['name', 'type', 'size_name', 'size', 'distance'], + 'collimation': ['name', 'lengths'], + 'process': ['name', 'date', 'description', 'term', 'notes'], + 'sample': ['name', 'sample_id', 'thickness', 'transmission', 'temperature', 'position', 'orientation', 'details'], + 'transmission_spectrum': ['name', 'timestamp', 'transmission', 'transmission_deviation'], + 'other': ['title', 'run', 'definition'] +} + +initial_metadata_dict = {key: {} for key, _ in metadata.items()} diff --git a/src/metadata_filename_gui/metadata_tree_widget.py b/src/metadata_filename_gui/metadata_tree_widget.py new file mode 100644 index 0000000000..55cae54c26 --- /dev/null +++ b/src/metadata_filename_gui/metadata_tree_widget.py @@ -0,0 +1,27 @@ +from PySide6.QtWidgets import QTreeWidget, QTreeWidgetItem, QLabel +from PySide6.QtCore import QAbstractItemModel +from metadata_filename_gui.metadata_component_selector import MetadataComponentSelector +from metadata_filename_gui.metadata_selector import MetadataSelector +from metadata_filename_gui.metadata_tree_data import metadata as metadata_categories +from sasdata.ascii_reader_metadata import AsciiReaderMetadata + +class MetadataTreeWidget(QTreeWidget): + def __init__(self, metadata: AsciiReaderMetadata): + super().__init__() + self.setColumnCount(2) + self.setHeaderLabels(['Name', 'Filename Components']) + self.metadata: AsciiReaderMetadata = metadata + + def draw_tree(self, full_filename: str): + self.clear() + for top_level, items in metadata_categories.items(): + top_level_item = QTreeWidgetItem([top_level]) + for metadatum in items: + # selector = MetadataComponentSelector(metadatum, self.metadata_dict) + selector = MetadataSelector(top_level, metadatum, self.metadata, full_filename) + metadatum_item = QTreeWidgetItem([metadatum]) + # selector.draw_options(options, metadata_dict.get(metadatum)) + top_level_item.addChild(metadatum_item) + self.setItemWidget(metadatum_item, 1, selector) + self.insertTopLevelItem(0, top_level_item) + self.expandAll()