diff --git a/src/gui_qt/_py/basewindow.py b/src/gui_qt/_py/basewindow.py index 8d23235..cca4f0f 100644 --- a/src/gui_qt/_py/basewindow.py +++ b/src/gui_qt/_py/basewindow.py @@ -372,6 +372,8 @@ class Ui_BaseWindow(object): self.action_cut_xaxis.setObjectName("action_cut_xaxis") self.action_cut_yaxis = QtWidgets.QAction(BaseWindow) self.action_cut_yaxis.setObjectName("action_cut_yaxis") + self.actionUse_script = QtWidgets.QAction(BaseWindow) + self.actionUse_script.setObjectName("actionUse_script") self.menuSave.addAction(self.actionSave) self.menuSave.addAction(self.actionExportGraphic) self.menuSave.addAction(self.action_save_fit_parameter) @@ -399,6 +401,7 @@ class Ui_BaseWindow(object): self.menuData.addAction(self.menuCut_to_visible_range.menuAction()) self.menuData.addSeparator() self.menuData.addAction(self.actionChange_datatypes) + self.menuData.addAction(self.actionUse_script) self.menuHelp.addAction(self.actionShow_error_log) self.menuHelp.addAction(self.actionUpdate) self.menuHelp.addAction(self.actionBugs) @@ -647,6 +650,7 @@ class Ui_BaseWindow(object): self.action_cut_xaxis.setToolTip(_translate("BaseWindow", "Remove data points outside visible x range.")) self.action_cut_yaxis.setText(_translate("BaseWindow", "y axis")) self.action_cut_yaxis.setToolTip(_translate("BaseWindow", "Remove data points outside visible y range. Uses real part of points.")) + self.actionUse_script.setText(_translate("BaseWindow", "Use script...")) from ..data.datawidget.datawidget import DataWidget from ..data.integral_widget import IntegralWidget from ..data.point_select import PointSelectWidget diff --git a/src/gui_qt/editors/__init__.py b/src/gui_qt/editors/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/gui_qt/lib/codeeditor.py b/src/gui_qt/editors/codeeditor.py similarity index 96% rename from src/gui_qt/lib/codeeditor.py rename to src/gui_qt/editors/codeeditor.py index f910b26..a90840f 100644 --- a/src/gui_qt/lib/codeeditor.py +++ b/src/gui_qt/editors/codeeditor.py @@ -187,10 +187,10 @@ class CodeEditor(QtWidgets.QPlainTextEdit): self.highlight = PythonHighlighter(self.document()) def keyPressEvent(self, evt): - if evt.key() == QtCore.Qt.Key_Tab: + if evt.key() == QtCore.Qt.Key.Key_Tab: # use spaces instead of tab self.insertPlainText(' '*4) - elif evt.key() == QtCore.Qt.Key_Insert: + elif evt.key() == QtCore.Qt.Key.Key_Insert: self.setOverwriteMode(not self.overwriteMode()) else: super().keyPressEvent(evt) @@ -225,7 +225,7 @@ class CodeEditor(QtWidgets.QPlainTextEdit): def paintevent_linenumber(self, evt): painter = QtGui.QPainter(self.current_linenumber) - painter.fillRect(evt.rect(), QtCore.Qt.lightGray) + painter.fillRect(evt.rect(), QtCore.Qt.GlobalColor.lightGray) block = self.firstVisibleBlock() block_number = block.blockNumber() @@ -237,9 +237,9 @@ class CodeEditor(QtWidgets.QPlainTextEdit): while block.isValid() and (top <= evt.rect().bottom()): if block.isVisible() and (bottom >= evt.rect().top()): number = str(block_number + 1) - painter.setPen(QtCore.Qt.black) + painter.setPen(QtCore.Qt.GlobalColor.black) painter.drawText(0, int(top), self.current_linenumber.width() - 3, height, - QtCore.Qt.AlignRight, number) + QtCore.Qt.AlignmentFlag.AlignRight, number) block = block.next() top = bottom @@ -252,7 +252,7 @@ class CodeEditor(QtWidgets.QPlainTextEdit): if not self.isReadOnly(): selection = QtWidgets.QTextEdit.ExtraSelection() - line_color = QtGui.QColor(QtCore.Qt.yellow).lighter(180) + line_color = QtGui.QColor(QtCore.Qt.GlobalColor.yellow).lighter(180) selection.format.setBackground(line_color) selection.format.setProperty(QtGui.QTextFormat.FullWidthSelection, True) diff --git a/src/gui_qt/editors/script_editor.py b/src/gui_qt/editors/script_editor.py new file mode 100644 index 0000000..ae3d175 --- /dev/null +++ b/src/gui_qt/editors/script_editor.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from pathlib import Path + +from .usermodeleditor import QUsermodelEditor +from ..Qt import QtWidgets, QtCore, QtGui + + +class QEditor(QUsermodelEditor): + runSignal = QtCore.pyqtSignal(str) + + def __init__(self, path: str | Path = None, parent=None): + super().__init__(path, parent=parent) + + self.add_run_button() + + def add_run_button(self): + self.disclaimer = QtWidgets.QLabel("This is work in progress and less than perfect :(") + self.disclaimer.setStyleSheet('QLabel {color: rgb(255, 0, 0); font-weight: bold; font-size: 2.5em;};') + self.centralwidget.layout().insertWidget(0, self.disclaimer) + + self.run_button = QtWidgets.QPushButton("Run") + self.centralwidget.layout().addWidget(self.run_button) + + self.run_button.clicked.connect(self.start_script) + + @QtCore.pyqtSlot() + def start_script(self): + self.runSignal.emit(self.edit_field.toPlainText()) + diff --git a/src/gui_qt/lib/usermodeleditor.py b/src/gui_qt/editors/usermodeleditor.py similarity index 74% rename from src/gui_qt/lib/usermodeleditor.py rename to src/gui_qt/editors/usermodeleditor.py index d9e5761..3cc1dbc 100644 --- a/src/gui_qt/lib/usermodeleditor.py +++ b/src/gui_qt/editors/usermodeleditor.py @@ -3,7 +3,7 @@ from __future__ import annotations from pathlib import Path from ..Qt import QtWidgets, QtCore, QtGui -from ..lib.codeeditor import EditorWidget +from .codeeditor import EditorWidget class QUsermodelEditor(QtWidgets.QMainWindow): @@ -50,18 +50,20 @@ class QUsermodelEditor(QtWidgets.QMainWindow): self.menuFile.addAction('Close', self.close, QtGui.QKeySequence.Quit) self.resize(800, 600) - self.setGeometry(QtWidgets.QStyle.alignedRect( - QtCore.Qt.LeftToRight, QtCore.Qt.AlignCenter, - self.size(), QtWidgets.qApp.desktop().availableGeometry() - )) + self.setGeometry( + QtWidgets.QStyle.alignedRect( + QtCore.Qt.LayoutDirection.LeftToRight, + QtCore.Qt.AlignmentFlag.AlignCenter, + self.size(), + QtWidgets.qApp.desktop().availableGeometry() + ) + ) - @property def is_modified(self): - return self.edit_field.document().isModified() + return self.edit_field.editor.document().isModified() - @is_modified.setter - def is_modified(self, val: bool): - self.edit_field.document().setModified(val) + def set_modified(self, val: bool): + self.edit_field.editor.document().setModified(val) @QtCore.pyqtSlot() def open_file(self): @@ -75,17 +77,22 @@ class QUsermodelEditor(QtWidgets.QMainWindow): def read_file(self, fname: str | Path): self.set_fname_opts(fname) - with self.fname.open('r') as f: - self.edit_field.setPlainText(f.read()) + if self.fname is not None: + with self.fname.open('r') as f: + self.edit_field.setPlainText(f.read()) def set_fname_opts(self, fname: str | Path): - self.fname = Path(fname) - self._dir = self.fname.parent - self.setWindowTitle('Edit ' + str(fname)) + fname = Path(fname) + if fname.is_file(): + self.fname = Path(fname) + self._dir = self.fname.parent + self.setWindowTitle('Edit ' + str(fname)) + elif fname.is_dir(): + self._dir = fname + - @property def changes_saved(self) -> bool: - if not self.is_modified: + if not self.is_modified(): return True ret = QtWidgets.QMessageBox.question(self, 'Time to think', @@ -97,9 +104,9 @@ class QUsermodelEditor(QtWidgets.QMainWindow): self.save_file() if ret == QtWidgets.QMessageBox.No: - self.is_modified = False + self.set_modified(False) - return not self.is_modified + return not self.is_modified() @QtCore.pyqtSlot() def save_file(self): @@ -111,9 +118,9 @@ class QUsermodelEditor(QtWidgets.QMainWindow): self.set_fname_opts(outfile) - self.is_modified = False + self.set_modified(False) - return self.is_modified + return self.is_modified() @QtCore.pyqtSlot() def overwrite_file(self): @@ -123,10 +130,10 @@ class QUsermodelEditor(QtWidgets.QMainWindow): self.modelsChanged.emit() - self.is_modified = False + self.set_modified(False) def closeEvent(self, evt: QtGui.QCloseEvent): - if not self.changes_saved: + if not self.changes_saved(): evt.ignore() else: super().closeEvent(evt) diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index e470691..756fb3f 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -12,12 +12,13 @@ from ..lib.forms import SelectionWidget class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): value_requested = QtCore.pyqtSignal(int) - def __init__(self, parent=None): + def __init__(self, func_id: int, parent=None): super().__init__(parent=parent) self.setupUi(self) self.func = None self.func_idx = None + self.func_id = func_id self.max_width = QtCore.QSize(0, 0) self.global_parameter = [] self.data_parameter = [] @@ -301,8 +302,10 @@ class ParameterSingleWidget(QtWidgets.QWidget): self.name = name self.parametername.setText(convert(name)) - self.parametername.setToolTip('If this is bold then this parameter is only for this data. ' - 'Otherwise, the general parameter is used and displayed') + self.parametername.setToolTip( + 'If this is bold then this parameter is only for this data. ' + 'Otherwise, the general parameter is used and displayed' + ) # self.value_line.setValidator(QtGui.QDoubleValidator()) self.value_line.textChanged.connect(lambda: self.valueChanged.emit(self.value) if self.value is not None else 0) diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index 6700493..9aeaed3 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -77,8 +77,12 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): """ w = self.param_widgets[idx] self.stackedWidget.removeWidget(w) + w.setParent(None) w.deleteLater() del self.param_widgets[idx] + _, func_id = self.functionwidget.get_selected() + + self.get_functions() self._current_function = None if len(self.param_widgets) == 0: @@ -104,7 +108,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): if function is None: return - dialog = QFitParameterWidget(self.stackedWidget) + dialog = QFitParameterWidget(function_id, self.stackedWidget) data_names = self.data_table.data_list(include_name=True) dialog.set_function(function, function_idx) @@ -206,9 +210,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): for m in self.models[model_id]: func_id = m['cnt'] - self.stackedWidget.removeWidget(self.param_widgets[func_id]) - - self.param_widgets.pop(func_id) + self.remove_function(func_id) self._complex.pop(model_id) self._func_list.pop(model_id) diff --git a/src/gui_qt/fit/function_creation_dialog.py b/src/gui_qt/fit/function_creation_dialog.py index 25562cd..caf7d42 100644 --- a/src/gui_qt/fit/function_creation_dialog.py +++ b/src/gui_qt/fit/function_creation_dialog.py @@ -8,9 +8,9 @@ from typing import Any import numpy as np -from gui_qt.Qt import QtCore, QtWidgets, QtGui -from gui_qt._py.fitcreationdialog import Ui_Dialog -from gui_qt.lib.namespace import QNamespaceWidget +from ..Qt import QtCore, QtWidgets, QtGui +from .._py.fitcreationdialog import Ui_Dialog +from ..editors.namespace import QNamespaceWidget __all__ = ['QUserFitCreator'] diff --git a/src/gui_qt/io/fcbatchreader.py b/src/gui_qt/io/fcbatchreader.py index a4444e6..c62acb2 100644 --- a/src/gui_qt/io/fcbatchreader.py +++ b/src/gui_qt/io/fcbatchreader.py @@ -88,7 +88,6 @@ class QFCReader(QtWidgets.QDialog, Ui_FCEval_dialog): def accept(self): items = [self.listWidget.item(i).text() for i in range(self.listWidget.count())] - print(items) if items: with busy_cursor(): self.read(items) diff --git a/src/gui_qt/lib/logger.py b/src/gui_qt/lib/logger.py index 346b638..e495ee1 100644 --- a/src/gui_qt/lib/logger.py +++ b/src/gui_qt/lib/logger.py @@ -3,7 +3,7 @@ from pathlib import Path from PyQt5 import QtWidgets -from .codeeditor import _make_textformats +from ..editors.codeeditor import _make_textformats from ..Qt import QtWidgets, QtCore, QtGui from nmreval.configs import config_paths diff --git a/src/gui_qt/lib/namespace.py b/src/gui_qt/lib/namespace.py index ae783ba..e164cd3 100644 --- a/src/gui_qt/lib/namespace.py +++ b/src/gui_qt/lib/namespace.py @@ -4,6 +4,8 @@ from collections import namedtuple import numpy as np +import nmreval + from nmreval import models from nmreval.configs import config_paths from nmreval.lib.importer import find_models, import_ @@ -28,6 +30,7 @@ class Namespace: 'y_err': (None, 'y error values'), 'fit': (None, 'dictionary of fit parameter', 'fit["PIKA"]'), 'np': (np, 'numpy module'), + 'nmreval': (nmreval, 'built-in classes and stuff') }, parents=('Basic', 'General'), ) diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index bf2825c..d4c28eb 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -985,13 +985,21 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): @QtCore.pyqtSlot(name='on_actionFunction_editor_triggered') def edit_models(self): if self.editor is None: - from ..lib.usermodeleditor import QUsermodelEditor + from ..editors.usermodeleditor import QUsermodelEditor self.editor = QUsermodelEditor(config_paths() / 'usermodels.py', parent=self) self.editor.modelsChanged.connect(lambda: self.fit_dialog.read_and_load_functions()) self.editor.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) self.editor.show() + @QtCore.pyqtSlot(name='on_actionUse_script_triggered') + def open_editor(self): + from ..editors.script_editor import QEditor + + editor = QEditor(self.path, parent=self) + editor.runSignal.connect(self.management.run_script) + editor.show() + @QtCore.pyqtSlot(list, bool) def extend_fit(self, sets: list, only_subplots: bool): if only_subplots: diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index 21715fa..31553e3 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -1033,7 +1033,7 @@ class UpperManagement(QtCore.QObject): else: data = self.data[sets[0]] if isinstance(data.data, new_type): - error_list.append(f'{data.name} is alreade of type {new_type.__name__}') + error_list.append(f'{data.name} is already of type {new_type.__name__}') continue new_data = new_type(data.x, np.zeros(data.x.size)) @@ -1067,6 +1067,8 @@ class UpperManagement(QtCore.QObject): @QtCore.pyqtSlot(list, list, bool) def eval_expression(self, cmds: list, set_ids: list, overwrite: bool): + if self.namespace is None: + self.namespace = self.get_namespace() ns = self.namespace.flatten() if overwrite: @@ -1099,13 +1101,28 @@ class UpperManagement(QtCore.QObject): if failures: err_msg = QtWidgets.QMessageBox(parent=self.sender()) - err_msg.setText('One or more errors occured during evaluation.') + err_msg.setText('One or more errors occurred during evaluation.') err_msg.setDetailedText('\n'.join(f'{d.name} failed with error: {err.args}' for d, err in failures)) err_msg.exec() self.sender().success = not failures self.sender().add_data(self.active_sets) + @QtCore.pyqtSlot(str) + def run_script(self, text): + self.namespace = self.get_namespace() + ns = self.namespace.flatten() + ns['return_list'] = [] + + # custom namespace must be available in global namespace of exec, otherwise imports do not work in functions + exec(text, ns, ns) + + new_sets = [] + for new_data in ns['return_list']: + new_sets.append(self.add(new_data)) + + self.newData.emit(new_sets, '') + @QtCore.pyqtSlot(list, dict) def create_from_function(self, cmds: list, opts: dict): ns = dict(self.namespace.flatten()) diff --git a/src/resources/_ui/basewindow.ui b/src/resources/_ui/basewindow.ui index 85c1f68..e2c4cab 100644 --- a/src/resources/_ui/basewindow.ui +++ b/src/resources/_ui/basewindow.ui @@ -192,6 +192,7 @@ + @@ -1049,6 +1050,11 @@ Remove data points outside visible y range. Uses real part of points. + + + Use script... + +