from __future__ import annotations import re from nmreval.io.asciireader import AsciiReader from nmreval.utils import NUMBER_RE, numbers_from_string from ..Qt import QtGui, QtCore, QtWidgets from .._py.asciidialog import Ui_ascii_reader class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader): data_read = QtCore.pyqtSignal(list) file_ext = ['.dat', '.txt'] def __init__(self, fname=None, parent=None): super().__init__(parent=parent) self.setupUi(self) self.reader = None self._matches = [] self.regex_input.setText(NUMBER_RE.pattern) self.custom_input.setValidator(QtGui.QDoubleValidator()) self.comment_textfield = QtWidgets.QPlainTextEdit(self.header_widget) self.comment_textfield.setReadOnly(True) pal = QtWidgets.QApplication.instance().palette() rgb = pal.color(pal.Base).getRgb()[:3] rgb2 = pal.color(pal.Text).getRgb()[:3] self.comment_textfield.setStyleSheet(f'background-color: rgb{rgb}; color: rgb{rgb2};') self.header_widget.setText('Header') self.header_widget.addWidget(self.comment_textfield) self.ascii_table.horizontalHeader().setStretchLastSection(True) self.buttonbox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self.apply) self.buttonbox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(self.accept) self.changestaggeredrange(0) self.ascii_table.contextMenuEvent = self.ctx_table self.ascii_table.horizontalHeader().setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.ascii_table.horizontalHeader().customContextMenuRequested.connect(self.ctx_table) self.skip = False if fname is not None: self.__call__(fname) def __call__(self, fname, *args, **kwargs): self.reader = AsciiReader(fname) self.check_filename(self.regex_input.text()) if self.skip: self.accept() return for i in [self.stag_lineEdit, self.start_lineedit, self.end_lineedit, self.comment_textfield, self.ascii_table, self.delay_lineedit]: i.clear() self.staggered_checkBox.setChecked(False) self.log_checkBox.setChecked(False) self.set_gui() self.set_column_names(1) self.skippy_checkbox.blockSignals(True) self.skippy_checkbox.setCheckState(QtCore.Qt.CheckState.Unchecked) self.skippy_checkbox.blockSignals(False) return self def set_gui(self): for text in self.reader.header: self.comment_textfield.appendPlainText(text) tmp = self.line_spinBox.value() if self.reader.header: self.line_spinBox.setMaximum(len(self.reader.header)) else: self.line_spinBox.setValue(0) self.line_spinBox.setEnabled(False) self.show_preview(10) self.line_spinBox.setValue(tmp) if self.reader.delays is not None: set_string = ''.join(str(d) + '\n' for d in self.reader.delays) self.delay_textfield.setPlainText(set_string) self.delay_lineedit.setText(str(len(self.reader.delays))) def ctx_table(self, _): menu = QtWidgets.QMenu(self) x_action = QtWidgets.QAction('Set as x', self) x_action.triggered.connect(lambda: self.set_columns('x')) y_action = QtWidgets.QAction('Set as y', self) y_action.triggered.connect(lambda: self.set_columns('y')) menu.addActions([x_action, y_action]) if self.deltay_lineEdit.isEnabled(): yerr_action = QtWidgets.QAction('Set as \u0394y', self) yerr_action.triggered.connect(lambda: self.set_columns('yerr')) menu.addAction(yerr_action) menu.popup(QtGui.QCursor.pos()) def set_columns(self, mode: str): cols = ' '.join([str(s.column()+1) for s in self.ascii_table.selectionModel().selectedColumns()]) try: lineedit = {'x': self.x_lineedit, 'y': self.y_lineedit, 'yerr': self.deltay_lineEdit}[mode] lineedit.setText(cols) except KeyError: pass @QtCore.pyqtSlot(int, name='on_preview_spinBox_valueChanged') def show_preview(self, line_no: int): preview, width, comments = self.reader.make_preview(line_no) self.ascii_table.setRowCount(min(line_no, len(preview))) self.ascii_table.setColumnCount(width + 1) for i, line in enumerate(preview): comment_line = comments[i] for j, field in enumerate(line): it = QtWidgets.QTableWidgetItem(field) self.ascii_table.setItem(i, j, it) it = QtWidgets.QTableWidgetItem(comment_line) self.ascii_table.setItem(i, len(line), it) self.ascii_table.resizeColumnsToContents() @QtCore.pyqtSlot(int, name='on_column_checkBox_stateChanged') @QtCore.pyqtSlot(int, name='on_line_spinBox_valueChanged') def set_column_names(self, _): self.ascii_table.setHorizontalHeaderLabels(map(str, range(1, self.ascii_table.columnCount() + 1))) if self.column_checkBox.isChecked() and self.line_spinBox.isEnabled(): header_line = self.reader.header[self.line_spinBox.value()-1] header_line = header_line.strip('\n\t\r, ') header_line = re.sub(r'[\t, ;]+(?!\w*})', ';', header_line) self.ascii_table.setHorizontalHeaderLabels(header_line.split(';')) @QtCore.pyqtSlot(int, name='on_staggered_checkBox_stateChanged') def changestaggeredrange(self, state: int): self.stag_lineEdit.setEnabled(state) @QtCore.pyqtSlot(name='on_pushButton_clicked') def calc_delays(self): try: start = float(self.start_lineedit.text()) stop = float(self.end_lineedit.text()) num_delays = int(self.delay_lineedit.text()) is_staggered = self.staggered_checkBox.isChecked() if is_staggered: staggered_range = int(self.stag_lineEdit.text()) else: staggered_range = 0 except ValueError: _ = QtWidgets.QMessageBox.information(self, 'No delays', 'Delays cannot be calculated: Not enough or wrong arguments.') return self.reader.calc_delays(start, stop, num_delays, log=self.log_checkBox.isChecked(), stagg=staggered_range, stag_size=staggered_range) if self.reader.delays is not None: set_string = ''.join(str(d) + '\n' for d in self.reader.delays) self.delay_textfield.setPlainText(set_string) @QtCore.pyqtSlot(name='on_delay_textfield_textChanged') def delays_changed(self): new_delays = str(self.delay_textfield.toPlainText()).rstrip('\n').split('\n') self.delay_lineedit.setText(str(len(new_delays))) if new_delays[0] == '': new_delays = None else: for k, v in enumerate(new_delays): try: new_delays[k] = float(v) except ValueError: new_delays[k] = -1 self.reader.delays = new_delays @QtCore.pyqtSlot() def accept(self): if self.apply(): super().accept() def apply(self): # default row for x is the first row, it will be superseded if an integer number is given. x = self.x_lineedit.text() is_valid = True if x: try: x = int(x)-1 except ValueError: pass else: x = None if not self.check_column_numbers(x, max(self.reader.width)): _ = QtWidgets.QMessageBox.information(self, 'Improper input', f'Input for x axis is invalid') return False try: y = [int(t)-1 for t in self.y_lineedit.text().split(' ')] except ValueError: y = None if not self.check_column_numbers(y, max(self.reader.width)): _ = QtWidgets.QMessageBox.information(self, 'Improper input', f'Input for y axis is invalid') return False try: y_err = [int(t)-1 for t in self.deltay_lineEdit.text().split(' ')] except ValueError: y_err = None mode = self.buttonGroup.checkedButton().text() if mode != 'Points': y_err = None if not self.check_column_numbers(y, max(self.reader.width)): _ = QtWidgets.QMessageBox.information(self, 'Improper input', f'Input for y_err axis is invalid') return False col_header = None if self.column_checkBox.isChecked(): col_header = [] for i in range(self.ascii_table.columnCount()): if self.ascii_table.horizontalHeaderItem(i) is not None: col_header.append(self.ascii_table.horizontalHeaderItem(i).text()) else: col_header.append(i) if y is not None and col_header is not None: col_header = [col_header[i] for i in range(len(col_header))] try: ret_dic = self.reader.export( x=x, y=y, yerr=y_err, mode=mode, col_names=col_header, num_value=self.get_numerical_value(), ) self.data_read.emit(ret_dic) except Exception as e: _ = QtWidgets.QMessageBox.information(self, 'Reading failed', f'Import data failed with\n {e.args[0]}') return False return True @QtCore.pyqtSlot(int, name='on_buttonGroup_buttonClicked') def show_error(self, val: int): self.deltay_lineEdit.setEnabled(val == -2) @QtCore.pyqtSlot(int, name='on_skippy_checkbox_stateChanged') def skip_next_dial(self, _: int): self.skip = self.skippy_checkbox.isChecked() @QtCore.pyqtSlot(str, name='on_regex_input_textChanged') def check_filename(self, pattern: str = NUMBER_RE.pattern): if self.reader is None: return success = True self._matches = [] if pattern: try: re_pattern = re.compile(pattern) except re.error: success = False else: self._matches = [m for m in re_pattern.finditer(str(self.reader.fname.stem))] else: success = False # matches exist and have numbers in them if self._matches and all([len(numbers_from_string(m.group())) for m in self._matches]): self.re_match_index.blockSignals(True) self.re_match_index.setMaximum(len(self._matches)) self.re_match_index.blockSignals(False) else: success = False if success: self.regex_input.setStyleSheet('color: rgb(0, 0, 0)') else: self.regex_input.setStyleSheet('background-color: rgba(255, 0, 0, 50)') self.show_match(self.re_match_index.value()) @QtCore.pyqtSlot(int, name='on_re_match_index_valueChanged') def show_match(self, idx: int = 0): fname = str(self.reader.fname.stem) if self._matches: m = self._matches[idx-1] self.label_8.setText(f'{fname[:m.start()]}{fname[m.start():m.end()]}{fname[m.end():]}') else: self.label_8.setText(fname) def get_numerical_value(self) -> float: val = 0 if self.re_button.isChecked() and self._matches: m = self._matches[self.re_match_index.value()-1] val = numbers_from_string(m.group()) # numbers_from returns list of floats we use first match if available val = val[0] if val else 0.0 elif self.custom_button.isChecked(): val = float(self.custom_input.text()) return val def check_column_numbers(self, values: int | list[int] | str | None, num_column: int) -> bool: is_valid = False if values is None: is_valid = True elif values == 'index': is_valid = True elif isinstance(values, int): is_valid = values < num_column elif isinstance(values, list): try: is_valid = all(v < num_column for v in values) except TypeError: is_valid = False return is_valid