From d07b85ae27bfb8697188ee01e3ade0da1bd9fa4e Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 26 Sep 2024 18:39:55 +0200 Subject: [PATCH] mvp for script runner --- src/gui_qt/_py/basewindow.py | 4 + src/gui_qt/editors/__init__.py | 0 src/gui_qt/{lib => editors}/codeeditor.py | 4 +- src/gui_qt/editors/script_editor.py | 137 ++++++++++++++++++ .../{lib => editors}/usermodeleditor.py | 14 +- src/gui_qt/fit/function_creation_dialog.py | 6 +- src/gui_qt/lib/logger.py | 2 +- src/gui_qt/main/mainwindow.py | 10 +- src/gui_qt/main/management.py | 18 ++- src/resources/_ui/basewindow.ui | 6 + 10 files changed, 188 insertions(+), 13 deletions(-) create mode 100644 src/gui_qt/editors/__init__.py rename src/gui_qt/{lib => editors}/codeeditor.py (99%) create mode 100644 src/gui_qt/editors/script_editor.py rename src/gui_qt/{lib => editors}/usermodeleditor.py (92%) 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 99% rename from src/gui_qt/lib/codeeditor.py rename to src/gui_qt/editors/codeeditor.py index 970b513..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) diff --git a/src/gui_qt/editors/script_editor.py b/src/gui_qt/editors/script_editor.py new file mode 100644 index 0000000..4bf4dd9 --- /dev/null +++ b/src/gui_qt/editors/script_editor.py @@ -0,0 +1,137 @@ +from __future__ import annotations + +from pathlib import Path + +from ..Qt import QtWidgets, QtCore, QtGui +from .codeeditor import EditorWidget + + +class QEditor(QtWidgets.QMainWindow): + runSignal = QtCore.pyqtSignal(str) + + def __init__(self, path: str | Path = None, parent=None): + super().__init__(parent=parent) + + self._init_gui() + + self.fname = None + self._dir = None + if path is not None: + self.read_file(path) + + def _init_gui(self): + self.centralwidget = QtWidgets.QWidget(self) + + layout = QtWidgets.QVBoxLayout(self.centralwidget) + layout.setContentsMargins(3, 3, 3, 3) + layout.setSpacing(3) + + self.edit_field = EditorWidget(self.centralwidget) + font = QtGui.QFont('default') + font.setStyleHint(font.Monospace) + font.setPointSize(10) + self.edit_field.setFont(font) + + layout.addWidget(self.edit_field) + self.setCentralWidget(self.centralwidget) + + self.statusbar = QtWidgets.QStatusBar(self) + self.setStatusBar(self.statusbar) + + self.menubar = self.menuBar() + + self.menuFile = QtWidgets.QMenu('File', self.menubar) + self.menubar.addMenu(self.menuFile) + + self.menuFile.addAction('Open...', self.open_file, QtGui.QKeySequence.Open) + self.menuFile.addAction('Save', self.overwrite_file, QtGui.QKeySequence.Save) + self.menuFile.addAction('Save as...', self.save_file, QtGui.QKeySequence('Ctrl+Shift+S')) + self.menuFile.addSeparator() + self.menuFile.addAction('Close', self.close, QtGui.QKeySequence.Quit) + + self.resize(800, 600) + self.setGeometry( + QtWidgets.QStyle.alignedRect( + QtCore.Qt.LayoutDirection.LeftToRight, + QtCore.Qt.AlignmentFlag.AlignCenter, + self.size(), + QtWidgets.qApp.desktop().availableGeometry() + ) + ) + + def is_modified(self): + return self.edit_field.editor.document().isModified() + + def set_modified(self, val: bool): + self.edit_field.editor.document().setModified(val) + + @QtCore.pyqtSlot() + def open_file(self): + overwrite = self.changes_saved + + if overwrite: + fname, _ = QtWidgets.QFileDialog.getOpenFileName(directory=str(self._dir)) + if fname: + self.read_file(fname) + + def read_file(self, fname: str | Path): + self.set_fname_opts(fname) + + 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): + fname = Path(fname) + if fname.is_file(): + self.fname = Path(fname) + self._dir = self.fname.parent + elif fname.is_dir(): + self._dir = fname + self.setWindowTitle('Edit ' + str(fname)) + + def changes_saved(self) -> bool: + if not self.is_modified(): + return True + + ret = QtWidgets.QMessageBox.question(self, 'Time to think', + '

The document was modified.

\n' + '

Do you want to save changes?

', + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | + QtWidgets.QMessageBox.Cancel) + if ret == QtWidgets.QMessageBox.Yes: + self.save_file() + + if ret == QtWidgets.QMessageBox.No: + self.set_modified(False) + + return not self.is_modified() + + @QtCore.pyqtSlot() + def save_file(self): + outfile, _ = QtWidgets.QFileDialog().getSaveFileName(parent=self, caption='Save file', directory=str(self._dir)) + + if outfile: + with open(outfile, 'w') as f: + f.write(self.edit_field.toPlainText()) + + self.set_fname_opts(outfile) + + self.set_modified(False) + + return self.is_modified() + + @QtCore.pyqtSlot() + def overwrite_file(self): + if self.fname is not None: + with self.fname.open('w') as f: + f.write(self.edit_field.toPlainText()) + + self.modelsChanged.emit() + + self.set_modified(False) + + def closeEvent(self, evt: QtGui.QCloseEvent): + self.runSignal.emit(self.edit_field.toPlainText()) + + super().closeEvent(evt) diff --git a/src/gui_qt/lib/usermodeleditor.py b/src/gui_qt/editors/usermodeleditor.py similarity index 92% rename from src/gui_qt/lib/usermodeleditor.py rename to src/gui_qt/editors/usermodeleditor.py index 79867ae..e912aa0 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,10 +50,14 @@ 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() + ) + ) def is_modified(self): return self.edit_field.editor.document().isModified() 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/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/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..ef2022d 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -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,27 @@ 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'] = [] + + exec(text, globals(), 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... + +