Merge branch 'script-editor' into dev

This commit is contained in:
Dominik Demuth 2024-09-29 19:19:15 +02:00
commit fc91bf83fe
14 changed files with 123 additions and 44 deletions

View File

@ -372,6 +372,8 @@ class Ui_BaseWindow(object):
self.action_cut_xaxis.setObjectName("action_cut_xaxis") self.action_cut_xaxis.setObjectName("action_cut_xaxis")
self.action_cut_yaxis = QtWidgets.QAction(BaseWindow) self.action_cut_yaxis = QtWidgets.QAction(BaseWindow)
self.action_cut_yaxis.setObjectName("action_cut_yaxis") 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.actionSave)
self.menuSave.addAction(self.actionExportGraphic) self.menuSave.addAction(self.actionExportGraphic)
self.menuSave.addAction(self.action_save_fit_parameter) 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.addAction(self.menuCut_to_visible_range.menuAction())
self.menuData.addSeparator() self.menuData.addSeparator()
self.menuData.addAction(self.actionChange_datatypes) self.menuData.addAction(self.actionChange_datatypes)
self.menuData.addAction(self.actionUse_script)
self.menuHelp.addAction(self.actionShow_error_log) self.menuHelp.addAction(self.actionShow_error_log)
self.menuHelp.addAction(self.actionUpdate) self.menuHelp.addAction(self.actionUpdate)
self.menuHelp.addAction(self.actionBugs) 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_xaxis.setToolTip(_translate("BaseWindow", "Remove data points outside visible x range."))
self.action_cut_yaxis.setText(_translate("BaseWindow", "y axis")) 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.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.datawidget.datawidget import DataWidget
from ..data.integral_widget import IntegralWidget from ..data.integral_widget import IntegralWidget
from ..data.point_select import PointSelectWidget from ..data.point_select import PointSelectWidget

View File

View File

@ -187,10 +187,10 @@ class CodeEditor(QtWidgets.QPlainTextEdit):
self.highlight = PythonHighlighter(self.document()) self.highlight = PythonHighlighter(self.document())
def keyPressEvent(self, evt): def keyPressEvent(self, evt):
if evt.key() == QtCore.Qt.Key_Tab: if evt.key() == QtCore.Qt.Key.Key_Tab:
# use spaces instead of tab # use spaces instead of tab
self.insertPlainText(' '*4) self.insertPlainText(' '*4)
elif evt.key() == QtCore.Qt.Key_Insert: elif evt.key() == QtCore.Qt.Key.Key_Insert:
self.setOverwriteMode(not self.overwriteMode()) self.setOverwriteMode(not self.overwriteMode())
else: else:
super().keyPressEvent(evt) super().keyPressEvent(evt)
@ -225,7 +225,7 @@ class CodeEditor(QtWidgets.QPlainTextEdit):
def paintevent_linenumber(self, evt): def paintevent_linenumber(self, evt):
painter = QtGui.QPainter(self.current_linenumber) 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 = self.firstVisibleBlock()
block_number = block.blockNumber() block_number = block.blockNumber()
@ -237,9 +237,9 @@ class CodeEditor(QtWidgets.QPlainTextEdit):
while block.isValid() and (top <= evt.rect().bottom()): while block.isValid() and (top <= evt.rect().bottom()):
if block.isVisible() and (bottom >= evt.rect().top()): if block.isVisible() and (bottom >= evt.rect().top()):
number = str(block_number + 1) 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, painter.drawText(0, int(top), self.current_linenumber.width() - 3, height,
QtCore.Qt.AlignRight, number) QtCore.Qt.AlignmentFlag.AlignRight, number)
block = block.next() block = block.next()
top = bottom top = bottom
@ -252,7 +252,7 @@ class CodeEditor(QtWidgets.QPlainTextEdit):
if not self.isReadOnly(): if not self.isReadOnly():
selection = QtWidgets.QTextEdit.ExtraSelection() 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.setBackground(line_color)
selection.format.setProperty(QtGui.QTextFormat.FullWidthSelection, True) selection.format.setProperty(QtGui.QTextFormat.FullWidthSelection, True)

View File

@ -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())

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from ..Qt import QtWidgets, QtCore, QtGui from ..Qt import QtWidgets, QtCore, QtGui
from ..lib.codeeditor import EditorWidget from .codeeditor import EditorWidget
class QUsermodelEditor(QtWidgets.QMainWindow): class QUsermodelEditor(QtWidgets.QMainWindow):
@ -50,18 +50,20 @@ class QUsermodelEditor(QtWidgets.QMainWindow):
self.menuFile.addAction('Close', self.close, QtGui.QKeySequence.Quit) self.menuFile.addAction('Close', self.close, QtGui.QKeySequence.Quit)
self.resize(800, 600) self.resize(800, 600)
self.setGeometry(QtWidgets.QStyle.alignedRect( self.setGeometry(
QtCore.Qt.LeftToRight, QtCore.Qt.AlignCenter, QtWidgets.QStyle.alignedRect(
self.size(), QtWidgets.qApp.desktop().availableGeometry() QtCore.Qt.LayoutDirection.LeftToRight,
)) QtCore.Qt.AlignmentFlag.AlignCenter,
self.size(),
QtWidgets.qApp.desktop().availableGeometry()
)
)
@property
def is_modified(self): def is_modified(self):
return self.edit_field.document().isModified() return self.edit_field.editor.document().isModified()
@is_modified.setter def set_modified(self, val: bool):
def is_modified(self, val: bool): self.edit_field.editor.document().setModified(val)
self.edit_field.document().setModified(val)
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def open_file(self): def open_file(self):
@ -75,17 +77,22 @@ class QUsermodelEditor(QtWidgets.QMainWindow):
def read_file(self, fname: str | Path): def read_file(self, fname: str | Path):
self.set_fname_opts(fname) self.set_fname_opts(fname)
with self.fname.open('r') as f: if self.fname is not None:
self.edit_field.setPlainText(f.read()) with self.fname.open('r') as f:
self.edit_field.setPlainText(f.read())
def set_fname_opts(self, fname: str | Path): def set_fname_opts(self, fname: str | Path):
self.fname = Path(fname) fname = Path(fname)
self._dir = self.fname.parent if fname.is_file():
self.setWindowTitle('Edit ' + str(fname)) 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: def changes_saved(self) -> bool:
if not self.is_modified: if not self.is_modified():
return True return True
ret = QtWidgets.QMessageBox.question(self, 'Time to think', ret = QtWidgets.QMessageBox.question(self, 'Time to think',
@ -97,9 +104,9 @@ class QUsermodelEditor(QtWidgets.QMainWindow):
self.save_file() self.save_file()
if ret == QtWidgets.QMessageBox.No: 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() @QtCore.pyqtSlot()
def save_file(self): def save_file(self):
@ -111,9 +118,9 @@ class QUsermodelEditor(QtWidgets.QMainWindow):
self.set_fname_opts(outfile) self.set_fname_opts(outfile)
self.is_modified = False self.set_modified(False)
return self.is_modified return self.is_modified()
@QtCore.pyqtSlot() @QtCore.pyqtSlot()
def overwrite_file(self): def overwrite_file(self):
@ -123,10 +130,10 @@ class QUsermodelEditor(QtWidgets.QMainWindow):
self.modelsChanged.emit() self.modelsChanged.emit()
self.is_modified = False self.set_modified(False)
def closeEvent(self, evt: QtGui.QCloseEvent): def closeEvent(self, evt: QtGui.QCloseEvent):
if not self.changes_saved: if not self.changes_saved():
evt.ignore() evt.ignore()
else: else:
super().closeEvent(evt) super().closeEvent(evt)

View File

@ -12,12 +12,13 @@ from ..lib.forms import SelectionWidget
class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit):
value_requested = QtCore.pyqtSignal(int) value_requested = QtCore.pyqtSignal(int)
def __init__(self, parent=None): def __init__(self, func_id: int, parent=None):
super().__init__(parent=parent) super().__init__(parent=parent)
self.setupUi(self) self.setupUi(self)
self.func = None self.func = None
self.func_idx = None self.func_idx = None
self.func_id = func_id
self.max_width = QtCore.QSize(0, 0) self.max_width = QtCore.QSize(0, 0)
self.global_parameter = [] self.global_parameter = []
self.data_parameter = [] self.data_parameter = []
@ -301,8 +302,10 @@ class ParameterSingleWidget(QtWidgets.QWidget):
self.name = name self.name = name
self.parametername.setText(convert(name)) self.parametername.setText(convert(name))
self.parametername.setToolTip('If this is bold then this parameter is only for this data. ' self.parametername.setToolTip(
'Otherwise, the general parameter is used and displayed') '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.setValidator(QtGui.QDoubleValidator())
self.value_line.textChanged.connect(lambda: self.valueChanged.emit(self.value) if self.value is not None else 0) self.value_line.textChanged.connect(lambda: self.valueChanged.emit(self.value) if self.value is not None else 0)

View File

@ -77,8 +77,12 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog):
""" """
w = self.param_widgets[idx] w = self.param_widgets[idx]
self.stackedWidget.removeWidget(w) self.stackedWidget.removeWidget(w)
w.setParent(None)
w.deleteLater() w.deleteLater()
del self.param_widgets[idx] del self.param_widgets[idx]
_, func_id = self.functionwidget.get_selected()
self.get_functions()
self._current_function = None self._current_function = None
if len(self.param_widgets) == 0: if len(self.param_widgets) == 0:
@ -104,7 +108,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog):
if function is None: if function is None:
return return
dialog = QFitParameterWidget(self.stackedWidget) dialog = QFitParameterWidget(function_id, self.stackedWidget)
data_names = self.data_table.data_list(include_name=True) data_names = self.data_table.data_list(include_name=True)
dialog.set_function(function, function_idx) dialog.set_function(function, function_idx)
@ -206,9 +210,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog):
for m in self.models[model_id]: for m in self.models[model_id]:
func_id = m['cnt'] func_id = m['cnt']
self.stackedWidget.removeWidget(self.param_widgets[func_id]) self.remove_function(func_id)
self.param_widgets.pop(func_id)
self._complex.pop(model_id) self._complex.pop(model_id)
self._func_list.pop(model_id) self._func_list.pop(model_id)

View File

@ -8,9 +8,9 @@ from typing import Any
import numpy as np import numpy as np
from gui_qt.Qt import QtCore, QtWidgets, QtGui from ..Qt import QtCore, QtWidgets, QtGui
from gui_qt._py.fitcreationdialog import Ui_Dialog from .._py.fitcreationdialog import Ui_Dialog
from gui_qt.lib.namespace import QNamespaceWidget from ..editors.namespace import QNamespaceWidget
__all__ = ['QUserFitCreator'] __all__ = ['QUserFitCreator']

View File

@ -88,7 +88,6 @@ class QFCReader(QtWidgets.QDialog, Ui_FCEval_dialog):
def accept(self): def accept(self):
items = [self.listWidget.item(i).text() for i in range(self.listWidget.count())] items = [self.listWidget.item(i).text() for i in range(self.listWidget.count())]
print(items)
if items: if items:
with busy_cursor(): with busy_cursor():
self.read(items) self.read(items)

View File

@ -3,7 +3,7 @@ from pathlib import Path
from PyQt5 import QtWidgets from PyQt5 import QtWidgets
from .codeeditor import _make_textformats from ..editors.codeeditor import _make_textformats
from ..Qt import QtWidgets, QtCore, QtGui from ..Qt import QtWidgets, QtCore, QtGui
from nmreval.configs import config_paths from nmreval.configs import config_paths

View File

@ -4,6 +4,8 @@ from collections import namedtuple
import numpy as np import numpy as np
import nmreval
from nmreval import models from nmreval import models
from nmreval.configs import config_paths from nmreval.configs import config_paths
from nmreval.lib.importer import find_models, import_ from nmreval.lib.importer import find_models, import_
@ -28,6 +30,7 @@ class Namespace:
'y_err': (None, 'y error values'), 'y_err': (None, 'y error values'),
'fit': (None, 'dictionary of fit parameter', 'fit["PIKA"]'), 'fit': (None, 'dictionary of fit parameter', 'fit["PIKA"]'),
'np': (np, 'numpy module'), 'np': (np, 'numpy module'),
'nmreval': (nmreval, 'built-in classes and stuff')
}, },
parents=('Basic', 'General'), parents=('Basic', 'General'),
) )

View File

@ -985,13 +985,21 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow):
@QtCore.pyqtSlot(name='on_actionFunction_editor_triggered') @QtCore.pyqtSlot(name='on_actionFunction_editor_triggered')
def edit_models(self): def edit_models(self):
if self.editor is None: 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 = QUsermodelEditor(config_paths() / 'usermodels.py', parent=self)
self.editor.modelsChanged.connect(lambda: self.fit_dialog.read_and_load_functions()) self.editor.modelsChanged.connect(lambda: self.fit_dialog.read_and_load_functions())
self.editor.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) self.editor.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal)
self.editor.show() 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) @QtCore.pyqtSlot(list, bool)
def extend_fit(self, sets: list, only_subplots: bool): def extend_fit(self, sets: list, only_subplots: bool):
if only_subplots: if only_subplots:

View File

@ -1033,7 +1033,7 @@ class UpperManagement(QtCore.QObject):
else: else:
data = self.data[sets[0]] data = self.data[sets[0]]
if isinstance(data.data, new_type): 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 continue
new_data = new_type(data.x, np.zeros(data.x.size)) new_data = new_type(data.x, np.zeros(data.x.size))
@ -1067,6 +1067,8 @@ class UpperManagement(QtCore.QObject):
@QtCore.pyqtSlot(list, list, bool) @QtCore.pyqtSlot(list, list, bool)
def eval_expression(self, cmds: list, set_ids: list, overwrite: 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() ns = self.namespace.flatten()
if overwrite: if overwrite:
@ -1099,13 +1101,28 @@ class UpperManagement(QtCore.QObject):
if failures: if failures:
err_msg = QtWidgets.QMessageBox(parent=self.sender()) 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.setDetailedText('\n'.join(f'{d.name} failed with error: {err.args}' for d, err in failures))
err_msg.exec() err_msg.exec()
self.sender().success = not failures self.sender().success = not failures
self.sender().add_data(self.active_sets) 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) @QtCore.pyqtSlot(list, dict)
def create_from_function(self, cmds: list, opts: dict): def create_from_function(self, cmds: list, opts: dict):
ns = dict(self.namespace.flatten()) ns = dict(self.namespace.flatten())

View File

@ -192,6 +192,7 @@
<addaction name="menuCut_to_visible_range"/> <addaction name="menuCut_to_visible_range"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionChange_datatypes"/> <addaction name="actionChange_datatypes"/>
<addaction name="actionUse_script"/>
</widget> </widget>
<widget class="QMenu" name="menuHelp"> <widget class="QMenu" name="menuHelp">
<property name="title"> <property name="title">
@ -1049,6 +1050,11 @@
<string>Remove data points outside visible y range. Uses real part of points.</string> <string>Remove data points outside visible y range. Uses real part of points.</string>
</property> </property>
</action> </action>
<action name="actionUse_script">
<property name="text">
<string>Use script...</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>