From 465fb0c09a67ee3e07e325472062655056049ec2 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 18 Jan 2024 18:25:07 +0000 Subject: [PATCH] 194-fitrange (#219) keyboard-setting of custom fit range; closes #194; helps for #32 --- src/gui_qt/_py/basewindow.py | 13 +- src/gui_qt/_py/fitmodelwidget.py | 9 +- src/gui_qt/fit/fit_forms.py | 176 +++++----------------------- src/gui_qt/fit/fit_parameter.py | 160 ++++++++++++++++++++++--- src/gui_qt/fit/fit_toolbar.py | 97 +++++++++++++++ src/gui_qt/fit/fitfunction.py | 6 +- src/gui_qt/fit/fitwindow.py | 13 +- src/gui_qt/fit/result.py | 2 +- src/gui_qt/lib/namespace.py | 80 +++++++------ src/gui_qt/main/mainwindow.py | 37 ++---- src/resources/_ui/basewindow.ui | 24 ---- src/resources/_ui/fitmodelwidget.ui | 25 ++++ 12 files changed, 368 insertions(+), 274 deletions(-) create mode 100644 src/gui_qt/fit/fit_toolbar.py diff --git a/src/gui_qt/_py/basewindow.py b/src/gui_qt/_py/basewindow.py index 5c0bff4..3b24795 100644 --- a/src/gui_qt/_py/basewindow.py +++ b/src/gui_qt/_py/basewindow.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'resources/_ui/basewindow.ui' +# Form implementation generated from reading ui file 'src/resources/_ui/basewindow.ui' # # Created by: PyQt5 UI code generator 5.15.10 # @@ -153,15 +153,6 @@ class Ui_BaseWindow(object): self.toolBar_nmr.setIconSize(QtCore.QSize(24, 24)) self.toolBar_nmr.setObjectName("toolBar_nmr") BaseWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar_nmr) - self.toolBar_fit = QtWidgets.QToolBar(BaseWindow) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.toolBar_fit.sizePolicy().hasHeightForWidth()) - self.toolBar_fit.setSizePolicy(sizePolicy) - self.toolBar_fit.setIconSize(QtCore.QSize(24, 24)) - self.toolBar_fit.setObjectName("toolBar_fit") - BaseWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar_fit) self.toolBar_spectrum = QtWidgets.QToolBar(BaseWindow) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -496,7 +487,6 @@ class Ui_BaseWindow(object): self.toolbar_edit.addAction(self.actionShift) self.toolBar_nmr.addAction(self.t1action) self.toolBar_nmr.addAction(self.actionCalculateT1) - self.toolBar_fit.addAction(self.action_FitWidget) self.toolBar_spectrum.addAction(self.action_edit) self.toolBar_spectrum.addAction(self.actionPick_position) self.toolBar_data.addAction(self.actionConcatenate_sets) @@ -537,7 +527,6 @@ class Ui_BaseWindow(object): self.toolBar.setWindowTitle(_translate("BaseWindow", "Main")) self.toolbar_edit.setWindowTitle(_translate("BaseWindow", "Math")) self.toolBar_nmr.setWindowTitle(_translate("BaseWindow", "NMR")) - self.toolBar_fit.setWindowTitle(_translate("BaseWindow", "Fit")) self.toolBar_spectrum.setWindowTitle(_translate("BaseWindow", "Spectrum")) self.toolBar_data.setWindowTitle(_translate("BaseWindow", "Data")) self.action_close.setText(_translate("BaseWindow", "&Quit")) diff --git a/src/gui_qt/_py/fitmodelwidget.py b/src/gui_qt/_py/fitmodelwidget.py index f183f36..8664646 100644 --- a/src/gui_qt/_py/fitmodelwidget.py +++ b/src/gui_qt/_py/fitmodelwidget.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'src/resources/_ui/fitmodelwidget.ui' # -# Created by: PyQt5 UI code generator 5.15.9 +# Created by: PyQt5 UI code generator 5.15.10 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -42,6 +42,7 @@ class Ui_FitParameter(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.parameter_line.sizePolicy().hasHeightForWidth()) self.parameter_line.setSizePolicy(sizePolicy) + self.parameter_line.setMaximumSize(QtCore.QSize(160, 16777215)) self.parameter_line.setText("") self.parameter_line.setObjectName("parameter_line") self.horizontalLayout_2.addWidget(self.parameter_line) @@ -51,6 +52,9 @@ class Ui_FitParameter(object): self.global_checkbox = QtWidgets.QCheckBox(FitParameter) self.global_checkbox.setObjectName("global_checkbox") self.horizontalLayout_2.addWidget(self.global_checkbox) + self.reset_button = QtWidgets.QPushButton(FitParameter) + self.reset_button.setObjectName("reset_button") + self.horizontalLayout_2.addWidget(self.reset_button) self.verticalLayout.addLayout(self.horizontalLayout_2) self.frame = QtWidgets.QFrame(FitParameter) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) @@ -82,6 +86,7 @@ class Ui_FitParameter(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.lineEdit.sizePolicy().hasHeightForWidth()) self.lineEdit.setSizePolicy(sizePolicy) + self.lineEdit.setMaximumSize(QtCore.QSize(100, 16777215)) self.lineEdit.setText("") self.lineEdit.setFrame(True) self.lineEdit.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) @@ -100,6 +105,7 @@ class Ui_FitParameter(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.lineEdit_2.sizePolicy().hasHeightForWidth()) self.lineEdit_2.setSizePolicy(sizePolicy) + self.lineEdit_2.setMaximumSize(QtCore.QSize(100, 16777215)) self.lineEdit_2.setText("") self.lineEdit_2.setFrame(True) self.lineEdit_2.setObjectName("lineEdit_2") @@ -122,6 +128,7 @@ class Ui_FitParameter(object): self.parameter_line.setPlaceholderText(_translate("FitParameter", "0")) self.fixed_check.setText(_translate("FitParameter", "Fix")) self.global_checkbox.setText(_translate("FitParameter", "Global")) + self.reset_button.setText(_translate("FitParameter", "Use global")) self.lineEdit.setToolTip(_translate("FitParameter", "

Lower bound. Same bound is used for all data. Leave empty for no boundary condition.

")) self.label_3.setText(_translate("FitParameter", "Textlabel")) self.lineEdit_2.setToolTip(_translate("FitParameter", "

Upper bound. Same bound is used for all data. Leave empty for no boundary condition.

")) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index 4b696c5..60c3191 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -1,138 +1,11 @@ from __future__ import annotations -from nmreval.utils.text import convert - from ..Qt import QtCore, QtWidgets, QtGui -from .._py.fitmodelwidget import Ui_FitParameter from .._py.save_fitmodel_dialog import Ui_SaveDialog from ..lib.iconloading import get_icon from ..lib.tables import TableWidget -class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): - """ - Widget to show a global parameter - """ - - value_requested = QtCore.pyqtSignal(object) - value_changed = QtCore.pyqtSignal(str) - state_changed = QtCore.pyqtSignal() - replace_single_value = QtCore.pyqtSignal(object) - - def __init__(self, label: str = 'Fitparameter', parent=None, fixed: bool = False): - super().__init__(parent) - self.setupUi(self) - - self.name = label - - self.parametername.setText(convert(label) + ' ') - - self.parameter_line.setText('1') - self.parameter_line.setMaximumWidth(160) - self.lineEdit.setMaximumWidth(100) - self.lineEdit_2.setMaximumWidth(100) - - self.label_3.setText(f'< {convert(label)} <') - - self.checkBox.stateChanged.connect(self.enableBounds) - self.global_checkbox.stateChanged.connect(lambda: self.state_changed.emit()) - self.parameter_line.editingFinished.connect(self.update_parameter) - self.parameter_line.values_requested.connect(lambda: self.value_requested.emit(self)) - self.parameter_line.replace_single_values.connect(lambda: self.replace_single_value.emit(None)) - self.parameter_line.editingFinished.connect(lambda: self.value_changed.emit(self.parameter_line.text())) - self.fixed_check.toggled.connect(self.set_fixed) - - if fixed: - self.fixed_check.hide() - - self.parameter_pos = None - self.func_idx = None - - self._linetext = '1' - - self.menu = QtWidgets.QMenu(self) - - def set_parameter_string(self, p: str): - self.parameter_line.setText(p) - self.parameter_line.setToolTip(p) - - def set_bounds(self, lb: float, ub: float, cbox: bool = True): - self.checkBox.setCheckState(QtCore.Qt.Checked if cbox else QtCore.Qt.Unchecked) - for val, bds_line in [(lb, self.lineEdit), (ub, self.lineEdit_2)]: - if val is not None: - bds_line.setText(str(val)) - else: - bds_line.setText('') - - def enableBounds(self, value: int): - self.lineEdit.setEnabled(value == 2) - self.lineEdit_2.setEnabled(value == 2) - - def set_parameter(self, p: float | None, bds: tuple[float, float, bool] = None, - fixed: bool = None, glob: bool = None): - ptext = f'{p:.4g}' - - self.set_parameter_string(ptext) - - if bds is not None: - self.set_bounds(*bds) - - if fixed is not None: - self.fixed_check.setCheckState(QtCore.Qt.CheckState.Unchecked if fixed else QtCore.Qt.CheckState.Checked) - - if glob is not None: - self.global_checkbox.setCheckState(QtCore.Qt.CheckState.Checked if glob else QtCore.Qt.CheckState.Unchecked) - - def get_parameter(self): - try: - p = float(self.parameter_line.text().replace(',', '.')) - except ValueError: - p = self.parameter_line.text().replace(',', '.') - - if self.checkBox.isChecked(): - lb_text = self.lineEdit.text() - lb = None - if lb_text: - try: - lb = float(lb_text.replace(',', '.')) - except ValueError: - lb = lb_text - - ub_text = self.lineEdit_2.text() - rb = None - if ub_text: - try: - rb = float(ub_text.replace(',', '.')) - except ValueError: - rb = ub_text - else: - lb = rb = None - - bounds = (lb, rb) - - return p, bounds, not self.fixed_check.isChecked(), self.global_checkbox.isChecked() - - @QtCore.pyqtSlot(bool) - def set_fixed(self, state: bool): - # self.global_checkbox.setVisible(not state) - self.frame.setVisible(not state) - - @QtCore.pyqtSlot() - def update_parameter(self): - new_value = self.parameter_line.text() - if not new_value: - self.parameter_line.setText('1') - - try: - float(new_value) - is_text = False - except ValueError: - is_text = True - self.global_checkbox.setCheckState(False) - - self.set_fixed(is_text or self.fixed_check.isChecked()) - - class QSaveModelDialog(QtWidgets.QDialog, Ui_SaveDialog): def __init__(self, types=None, parent=None): super().__init__(parent=parent) @@ -172,30 +45,37 @@ class FitModelTree(QtWidgets.QTreeWidget): treeChanged = QtCore.pyqtSignal() itemRemoved = QtCore.pyqtSignal(int) - counterRole = QtCore.Qt.UserRole + 1 - operatorRole = QtCore.Qt.UserRole + 2 + counterRole = QtCore.Qt.ItemDataRole.UserRole + 1 + operatorRole = QtCore.Qt.ItemDataRole.UserRole + 2 def __init__(self, parent=None): super().__init__(parent=parent) self.setHeaderHidden(True) self.setDragEnabled(True) self.setDragDropMode(QtWidgets.QTreeWidget.InternalMove) - self.setDefaultDropAction(QtCore.Qt.MoveAction) + self.setDefaultDropAction(QtCore.Qt.DropAction.MoveAction) self.itemSelectionChanged.connect(lambda: self.treeChanged.emit()) def keyPressEvent(self, evt): - operators = [QtCore.Qt.Key_Plus, QtCore.Qt.Key_Asterisk, - QtCore.Qt.Key_Minus, QtCore.Qt.Key_Slash] + operators = [ + QtCore.Qt.Key.Key_Plus, + QtCore.Qt.Key.Key_Asterisk, + QtCore.Qt.Key.Key_Minus, + QtCore.Qt.Key.Key_Slash + ] - if evt.key() == QtCore.Qt.Key_Delete: + if evt.key() == QtCore.Qt.Key.Key_Delete: for item in self.selectedItems(): self.remove_function(item) - elif evt.key() == QtCore.Qt.Key_Space: + elif evt.key() == QtCore.Qt.Key.Key_Space: for item in self.treeWidget.selectedItems(): - item.setCheckState(0, QtCore.Qt.Checked) if item.checkState( - 0) == QtCore.Qt.Unchecked else item.setCheckState(0, QtCore.Qt.Unchecked) + cs = item.checkState(0) + if cs == QtCore.Qt.CheckState.Unchecked: + item.setCheckState(0, QtCore.Qt.CheckState.Checked) + else: + item.setCheckState(0, QtCore.Qt.CheckState.Unchecked) elif evt.key() in operators: idx = operators.index(evt.key()) @@ -246,7 +126,7 @@ class FitModelTree(QtWidgets.QTreeWidget): color = QtGui.QColor(color) it = QtWidgets.QTreeWidgetItem() - it.setData(0, QtCore.Qt.UserRole, idx) + it.setData(0, QtCore.Qt.ItemDataRole.UserRole, idx) it.setData(0, self.counterRole, cnt) it.setData(0, self.operatorRole, op) it.setText(0, name) @@ -257,7 +137,7 @@ class FitModelTree(QtWidgets.QTreeWidget): it.setForeground(0, QtGui.QBrush(color)) it.setIcon(0, get_icon(self.icons[op])) - it.setCheckState(0, QtCore.Qt.Checked if active else QtCore.Qt.Unchecked) + it.setCheckState(0, QtCore.Qt.CheckState.Checked if active else QtCore.Qt.CheckState.Unchecked) if parent is None: self.addTopLevelItem(it) @@ -277,7 +157,7 @@ class FitModelTree(QtWidgets.QTreeWidget): def get_selected(self): try: it = self.selectedItems()[0] - function_nr = it.data(0, QtCore.Qt.UserRole) + function_nr = it.data(0, QtCore.Qt.ItemDataRole.UserRole) idx = it.data(0, self.counterRole) except IndexError: @@ -300,10 +180,10 @@ class FitModelTree(QtWidgets.QTreeWidget): it = parent.child(i) child = { - 'idx': it.data(0, QtCore.Qt.UserRole), + 'idx': it.data(0, QtCore.Qt.ItemDataRole.UserRole), 'op': it.data(0, self.operatorRole), 'pos': pos, - 'active': (it.checkState(0) == QtCore.Qt.Checked), + 'active': (it.checkState(0) == QtCore.Qt.CheckState.Checked), 'children': [] } @@ -371,8 +251,8 @@ class FitTableWidget(TableWidget): for (sid, name) in set_ids: item = QtWidgets.QTableWidgetItem(name) - item.setCheckState(QtCore.Qt.Checked) - item.setData(QtCore.Qt.UserRole+1, sid) + item.setCheckState(QtCore.Qt.CheckState.Checked) + item.setData(QtCore.Qt.ItemDataRole.UserRole+1, sid) row = self.rowCount() self.setRowCount(row+1) self.setItem(row, 0, item) @@ -390,15 +270,15 @@ class FitTableWidget(TableWidget): for i in range(self.rowCount()): item = self.item(i, 0) - if item.checkState() == QtCore.Qt.Checked: + if item.checkState() == QtCore.Qt.CheckState.Checked: mod = self.cellWidget(i, 1).currentData() if mod is None: mod = default if include_name: - arg = (item.data(QtCore.Qt.UserRole+1), item.text()) + arg = (item.data(QtCore.Qt.ItemDataRole.UserRole+1), item.text()) else: - arg = item.data(QtCore.Qt.UserRole+1) + arg = item.data(QtCore.Qt.ItemDataRole.UserRole+1) if mod not in data: data[mod] = [] @@ -411,8 +291,8 @@ class FitTableWidget(TableWidget): for i in range(self.rowCount()): item = self.item(i, 0) if include_name: - ret_val.append((item.data(QtCore.Qt.UserRole+1), item.text())) + ret_val.append((item.data(QtCore.Qt.ItemDataRole.UserRole+1), item.text())) else: - ret_val.append(item.data(QtCore.Qt.UserRole+1)) + ret_val.append(item.data(QtCore.Qt.ItemDataRole.UserRole+1)) return ret_val diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index e4a72d2..e470691 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -1,14 +1,12 @@ from __future__ import annotations -from typing import Optional - from nmreval.fit.parameter import Parameter from nmreval.utils.text import convert from ..Qt import QtWidgets, QtCore, QtGui from .._py.fitfuncwidget import Ui_FormFit +from .._py.fitmodelwidget import Ui_FitParameter from ..lib.forms import SelectionWidget -from .fit_forms import FitModelWidget class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): @@ -30,16 +28,15 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): self.scrollwidget2.setLayout(QtWidgets.QVBoxLayout()) def eventFilter(self, src: QtCore.QObject, evt: QtCore.QEvent): + modifiers = QtCore.Qt.KeyboardModifier.ControlModifier | QtCore.Qt.KeyboardModifier.ShiftModifier if isinstance(evt, QtGui.QKeyEvent): - if (evt.key() == QtCore.Qt.Key_Right) and \ - (evt.modifiers() == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier): + if (evt.key() == QtCore.Qt.Key.Key_Right) and (evt.modifiers() == modifiers): self.change_single_parameter(src.value, sender=src) self.select_next_preview(1) return True - elif (evt.key() == QtCore.Qt.Key_Left) and \ - (evt.modifiers() == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier): + elif (evt.key() == QtCore.Qt.Key.Key_Left) and (evt.modifiers() == modifiers): self.change_single_parameter(src.value, sender=src) self.select_next_preview(-1) @@ -65,7 +62,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): self.glob_values = [1] * len(func.params) for k, v in enumerate(func.params): - widgt = FitModelWidget(label=v, parent=self.scrollwidget) + widgt = ParameterGlobalWidget(name=v, parent=self.scrollwidget) widgt.parameter_pos = k widgt.func_idx = idx try: @@ -95,7 +92,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): for w1, w2 in zip(self.global_parameter, self.data_parameter): w1.parametername.setFixedSize(self.max_width) w1.checkBox.setFixedSize(self.max_width) - w2.label.setFixedSize(self.max_width) + w2.parametername.setFixedSize(self.max_width) if hasattr(func, 'choices') and func.choices is not None: cbox = func.choices @@ -175,7 +172,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): # disable single parameter if it is set global, enable if global is unset widget = self.sender() idx = self.global_parameter.index(widget) - enable = (widget.global_checkbox.checkState() == QtCore.Qt.Unchecked) + enable = (widget.global_checkbox.checkState() == QtCore.Qt.CheckState.Unchecked) self.data_parameter[idx].setEnabled(enable) def select_next_preview(self, direction): @@ -215,7 +212,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): param_general = [] for g in self.global_parameter: - if isinstance(g, FitModelWidget): + if isinstance(g, ParameterGlobalWidget): p_i, bds_i, fixed_i, global_i = g.get_parameter() parameter_i = Parameter(name=g.name, value=p_i, lb=bds_i[0], ub=bds_i[1], var=fixed_i) param_general.append(parameter_i) @@ -236,7 +233,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): p = [] for i, (p_i, g) in enumerate(zip(parameter, self.global_parameter)): - if isinstance(g, FitModelWidget): + if isinstance(g, ParameterGlobalWidget): if (p_i is None) or is_global[i]: # set has no oen value p.append(param_general[i].copy()) @@ -303,8 +300,8 @@ class ParameterSingleWidget(QtWidgets.QWidget): self._init_ui() self.name = name - self.label.setText(convert(name)) - self.label.setToolTip('If this is bold then this parameter is only for this data. ' + 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.value_line.setValidator(QtGui.QDoubleValidator()) @@ -316,8 +313,8 @@ class ParameterSingleWidget(QtWidgets.QWidget): layout.setContentsMargins(2, 2, 2, 2) layout.setSpacing(2) - self.label = QtWidgets.QLabel(self) - layout.addWidget(self.label) + self.parametername = QtWidgets.QLabel(self) + layout.addWidget(self.parametername) layout.addSpacerItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)) @@ -347,6 +344,133 @@ class ParameterSingleWidget(QtWidgets.QWidget): def show_as_local_parameter(self, is_local: bool): if is_local: - self.label.setStyleSheet('font-weight: bold;') + self.parametername.setStyleSheet('font-weight: bold;') else: - self.label.setStyleSheet('') + self.parametername.setStyleSheet('') + + +class ParameterGlobalWidget(QtWidgets.QWidget, Ui_FitParameter): + """ + Widget to show a global parameter + """ + + value_requested = QtCore.pyqtSignal(object) + value_changed = QtCore.pyqtSignal(str) + state_changed = QtCore.pyqtSignal() + replace_single_value = QtCore.pyqtSignal(object) + + def __init__(self, name: str = 'Fitparameter', parent=None, fixed: bool = False): + super().__init__(parent) + self.setupUi(self) + + self.name = name + self.reset_button.setVisible(False) + + self.parametername.setText(convert(name) + ' ') + + self.parameter_line.setText('1') + self.parameter_line.setMaximumWidth(160) + self.lineEdit.setMaximumWidth(100) + self.lineEdit_2.setMaximumWidth(100) + + self.label_3.setText(f'< {convert(name)} <') + + self.checkBox.stateChanged.connect(self.enableBounds) + self.global_checkbox.stateChanged.connect(lambda: self.state_changed.emit()) + self.parameter_line.editingFinished.connect(self.update_parameter) + self.parameter_line.values_requested.connect(lambda: self.value_requested.emit(self)) + self.parameter_line.replace_single_values.connect(lambda: self.replace_single_value.emit(None)) + self.parameter_line.editingFinished.connect(lambda: self.value_changed.emit(self.parameter_line.text())) + self.fixed_check.toggled.connect(self.set_fixed) + + if fixed: + self.fixed_check.hide() + + self.reset_button.setVisible(False) + + self.parameter_pos = None + self.func_idx = None + + self._linetext = '1' + + self.menu = QtWidgets.QMenu(self) + + def set_parameter_string(self, p: str): + self.parameter_line.setText(p) + self.parameter_line.setToolTip(p) + + def set_bounds(self, lb: float, ub: float, cbox: bool = True): + self.checkBox.setCheckState(QtCore.Qt.CheckState.Checked if cbox else QtCore.Qt.CheckState.Unchecked) + for val, bds_line in [(lb, self.lineEdit), (ub, self.lineEdit_2)]: + if val is not None: + bds_line.setText(str(val)) + else: + bds_line.setText('') + + def enableBounds(self, value: int): + self.lineEdit.setEnabled(value == 2) + self.lineEdit_2.setEnabled(value == 2) + + def set_parameter(self, p: float | None, bds: tuple[float, float, bool] = None, + fixed: bool = None, glob: bool = None): + ptext = f'{p:.4g}' + + self.set_parameter_string(ptext) + + if bds is not None: + self.set_bounds(*bds) + + if fixed is not None: + self.fixed_check.setCheckState(QtCore.Qt.CheckState.Unchecked if fixed else QtCore.Qt.CheckState.Checked) + + if glob is not None: + self.global_checkbox.setCheckState(QtCore.Qt.CheckState.Checked if glob else QtCore.Qt.CheckState.Unchecked) + + def get_parameter(self): + try: + p = float(self.parameter_line.text().replace(',', '.')) + except ValueError: + p = self.parameter_line.text().replace(',', '.') + + if self.checkBox.isChecked(): + lb_text = self.lineEdit.text() + lb = None + if lb_text: + try: + lb = float(lb_text.replace(',', '.')) + except ValueError: + lb = lb_text + + ub_text = self.lineEdit_2.text() + rb = None + if ub_text: + try: + rb = float(ub_text.replace(',', '.')) + except ValueError: + rb = ub_text + else: + lb = rb = None + + bounds = (lb, rb) + + return p, bounds, not self.fixed_check.isChecked(), self.global_checkbox.isChecked() + + @QtCore.pyqtSlot(bool) + def set_fixed(self, state: bool): + # self.global_checkbox.setVisible(not state) + self.frame.setVisible(not state) + + @QtCore.pyqtSlot() + def update_parameter(self): + new_value = self.parameter_line.text() + if not new_value: + self.parameter_line.setText('1') + + try: + float(new_value) + is_text = False + except ValueError: + is_text = True + self.global_checkbox.setCheckState(False) + + self.set_fixed(is_text or self.fixed_check.isChecked()) diff --git a/src/gui_qt/fit/fit_toolbar.py b/src/gui_qt/fit/fit_toolbar.py new file mode 100644 index 0000000..4cfa091 --- /dev/null +++ b/src/gui_qt/fit/fit_toolbar.py @@ -0,0 +1,97 @@ +from ..Qt import QtWidgets, QtGui, QtCore +from ..lib.iconloading import get_icon +from ..lib.pg_objects import RegionItem + + +class FitToolbar(QtWidgets.QToolBar): + def __init__( + self, + fitaction: QtWidgets.QAction, + limit_menu: QtWidgets.QMenu, + parent=None, + ): + super().__init__(parent=parent) + + self.fit_action = fitaction + self.region = RegionItem() + self.addAction(fitaction) + + self.fitlim_button = QtWidgets.QToolButton(self) + self.fitlim_button.setMenu(limit_menu) + self.fitlim_button.setPopupMode(self.fitlim_button.InstantPopup) + self.fitlim_button.setIcon(get_icon('fit_region')) + self.addWidget(self.fitlim_button) + + self.label = QtWidgets.QLabel(self) + self.label.setText('L: ') + self.addWidget(self.label) + self.label.setEnabled(False) + + self.lineedit = QtWidgets.QLineEdit(self) + self.lineedit.setValidator(QtGui.QDoubleValidator()) + self.lineedit.setMaximumWidth(92) + self.addWidget(self.lineedit) + self.lineedit.setEnabled(False) + + self.label2 = QtWidgets.QLabel(self) + self.label2.setText(' R: ') + self.addWidget(self.label2) + self.label2.setEnabled(False) + + self.lineedit2 = QtWidgets.QLineEdit(self) + self.lineedit2.setValidator(QtGui.QDoubleValidator()) + self.addWidget(self.lineedit2) + self.lineedit2.setMaximumWidth(92) + self.lineedit2.setEnabled(False) + + self.limit_group = QtWidgets.QActionGroup(self) + for ac in limit_menu.actions(): + self.limit_group.addAction(ac) + + self.limit_group.triggered.connect(self.change_limit_type) + + self.region.sigRegionChanged.connect(self.change_labels) + self.change_labels() + + self.lineedit.textChanged.connect(self.move_region) + self.lineedit2.textChanged.connect(self.move_region) + + @QtCore.pyqtSlot(QtWidgets.QAction) + def change_limit_type(self, action: QtWidgets.QAction): + is_custom = (action.text() == 'Custom') + + for w in [self.label, self.label2, self.lineedit, self.lineedit2]: + w.setEnabled(is_custom) + + def change_labels(self): + r = self.region.getRegion() + self.lineedit.blockSignals(True) + self.lineedit.setText(f'{r[0]:.4g}') + self.lineedit.blockSignals(False) + + self.lineedit2.blockSignals(True) + self.lineedit2.setText(f'{r[1]:.4g}') + self.lineedit2.blockSignals(False) + + def move_region(self): + try: + r_min = float(self.lineedit.text()) + except ValueError: + r_min = None + + try: + r_max = float(self.lineedit2.text()) + except ValueError: + r_max = None + + if r_min is not None and r_max is not None: + self.region.setRegion((r_min, r_max), use_log=True) + + def get_limit(self): + action_text = self.limit_group.checkedAction().text() + + return { + 'None': 'none', + 'Visible x range': 'x', + 'Custom': self.region.getRegion(), + }[action_text] diff --git a/src/gui_qt/fit/fitfunction.py b/src/gui_qt/fit/fitfunction.py index 54ce567..b524521 100644 --- a/src/gui_qt/fit/fitfunction.py +++ b/src/gui_qt/fit/fitfunction.py @@ -165,7 +165,7 @@ class QFunctionWidget(QtWidgets.QWidget, Ui_Form): self.iscomplex = False while iterator.value(): item = iterator.value() - f = self.functions[item.data(0, QtCore.Qt.UserRole)] + f = self.functions[item.data(0, QtCore.Qt.ItemDataRole.UserRole)] if hasattr(f, 'iscomplex') and f.iscomplex: self.iscomplex = True break @@ -226,7 +226,7 @@ class QFunctionWidget(QtWidgets.QWidget, Ui_Form): iterator = QtWidgets.QTreeWidgetItemIterator(self.functree) while iterator.value(): item = iterator.value() - f = self.functions[item.data(0, QtCore.Qt.UserRole)] + f = self.functions[item.data(0, QtCore.Qt.ItemDataRole.UserRole)] cnt = item.data(0, self.functree.counterRole) all_parameters[f'{f.name}_{cnt}'] = [(convert(pp, new='str'), (cnt, i)) for i, pp in enumerate(f.params)] @@ -240,7 +240,7 @@ class QFunctionWidget(QtWidgets.QWidget, Ui_Form): while iterator.value(): item = iterator.value() if item.checkState(0) != QtCore.Qt.CheckState.Unchecked: - f = self.functions[item.data(0, QtCore.Qt.UserRole)] + f = self.functions[item.data(0, QtCore.Qt.ItemDataRole.UserRole)] if hasattr(f, 'iscomplex') and f.iscomplex: iscomplex = True break diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index ac1f11e..e71e2be 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -9,7 +9,6 @@ import numpy as np from pyqtgraph import mkPen from nmreval.fit._meta import MultiModel, ModelFactory -from nmreval.fit.data import Data from nmreval.fit.model import Model from nmreval.fit.parameter import Parameters from nmreval.fit.result import FitResult @@ -42,8 +41,8 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): self._management = mgmt self._current_model = next(QFitDialog.model_cnt) - self.show_combobox.setItemData(0, self._current_model, QtCore.Qt.UserRole) - self.default_combobox.setItemData(0, self._current_model, QtCore.Qt.UserRole) + self.show_combobox.setItemData(0, self._current_model, QtCore.Qt.ItemDataRole.UserRole) + self.default_combobox.setItemData(0, self._current_model, QtCore.Qt.ItemDataRole.UserRole) self.data_table = FitTableWidget(self.data_widget) self.data_widget.addWidget(self.data_table) @@ -150,9 +149,9 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): # deselect all fit sets for i in range(self.data_table.rowCount()): - data_id = self.data_table.item(i, 0).data(QtCore.Qt.UserRole+1) + data_id = self.data_table.item(i, 0).data(QtCore.Qt.ItemDataRole.UserRole+1) if self._management[data_id].mode == 'fit' or self._management[data_id].has_relation(Relations.isFitPartOf): - self.data_table.item(i, 0).setCheckState(QtCore.Qt.Unchecked) + self.data_table.item(i, 0).setCheckState(QtCore.Qt.CheckState.Unchecked) if self.models: for m in self.models.keys(): @@ -176,7 +175,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): self.default_combobox.addItem('Model '+idx, userData=idx) self.show_combobox.addItem('Model '+idx, userData=idx) - self.show_combobox.setItemData(self.show_combobox.count()-1, idx, QtCore.Qt.UserRole) + self.show_combobox.setItemData(self.show_combobox.count()-1, idx, QtCore.Qt.ItemDataRole.UserRole) self.show_combobox.setCurrentIndex(self.show_combobox.count()-1) self._current_model = idx @@ -190,7 +189,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): self.get_functions() self.functionwidget.clear() - self._current_model = self.show_combobox.itemData(idx, QtCore.Qt.UserRole) + self._current_model = self.show_combobox.itemData(idx, QtCore.Qt.ItemDataRole.UserRole) if self._current_model in self.models and len(self.models[self._current_model]): for el in self.models[self._current_model]: self.functionwidget.add_function(**el) diff --git a/src/gui_qt/fit/result.py b/src/gui_qt/fit/result.py index eeaa56b..17b3799 100644 --- a/src/gui_qt/fit/result.py +++ b/src/gui_qt/fit/result.py @@ -217,8 +217,8 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): if idx in self.graph_opts: view_range, logx, logy = self.graph_opts[idx] - self.fit_plot.setLogMode(x=logx, y=logy) self.fit_plot.setRange(xRange=view_range[0], yRange=view_range[1], padding=0) + self.fit_plot.setLogMode(x=logx, y=logy) self.logx_box.blockSignals(True) self.logx_box.setChecked(logx) self.logx_box.blockSignals(False) diff --git a/src/gui_qt/lib/namespace.py b/src/gui_qt/lib/namespace.py index e6ce2eb..ae783ba 100644 --- a/src/gui_qt/lib/namespace.py +++ b/src/gui_qt/lib/namespace.py @@ -22,51 +22,59 @@ class Namespace: if basic: self.add_namespace( - {'x': (None, 'x values'), - 'y': (None, 'x values'), - 'y_err': (None, 'y error values'), - 'fit': (None, 'dictionary of fit parameter', 'fit["PIKA"]'), - 'np': (np, 'numpy module'), + { + 'x': (None, 'x values'), + 'y': (None, 'x values'), + 'y_err': (None, 'y error values'), + 'fit': (None, 'dictionary of fit parameter', 'fit["PIKA"]'), + 'np': (np, 'numpy module'), }, parents=('Basic', 'General'), ) self.add_namespace( - {'sin': (np.sin, 'Sine', 'sin(PIKA)'), - 'cos': (np.cos, 'Cosine', 'cos(PIKA)'), - 'tan': (np.tan, 'Tangens', 'tan(PIKA)'), - 'ln': (np.log, 'Natural Logarithm', 'ln(PIKA)'), - 'log': (np.log10, 'Logarithm (base 10)', 'log(PIKA)'), - 'exp': (np.exp, 'Exponential', 'exp(PIKA)'), - 'sqrt': (np.sqrt, 'Root', 'sqrt(PIKA)'), - 'lin_range': (np.linspace, 'N evenly spaced over interval [start, stop]', 'lin_range(start, stop, N)'), - 'log_range': (np.geomspace, 'N evenly spaced (log-scale) over interval [start, stop]', 'lin_range(start, stop, N)'), - }, + { + 'sin': (np.sin, 'Sine', 'sin(PIKA)'), + 'cos': (np.cos, 'Cosine', 'cos(PIKA)'), + 'tan': (np.tan, 'Tangens', 'tan(PIKA)'), + 'ln': (np.log, 'Natural Logarithm', 'ln(PIKA)'), + 'log': (np.log10, 'Logarithm (base 10)', 'log(PIKA)'), + 'exp': (np.exp, 'Exponential', 'exp(PIKA)'), + 'sqrt': (np.sqrt, 'Root', 'sqrt(PIKA)'), + 'lin_range': (np.linspace, 'N evenly spaced over interval [start, stop]', 'lin_range(start, stop, N)'), + 'log_range': (np.geomspace, 'N evenly spaced (log-scale) over interval [start, stop]', 'lin_range(start, stop, N)'), + }, parents=('Basic', 'Functions')) self.add_namespace( - {'max': (np.max, 'Maximum value', 'max(PIKA)'), - 'min': (np.min, 'Minimum value', 'min(PIKA)'), - 'argmax': (np.argmax, 'Index of maximum value', 'argmax(PIKA)'), - 'argmin': (np.argmax, 'Index of minimum value', 'argmin(PIKA)'), - }, + { + 'max': (np.max, 'Maximum value', 'max(PIKA)'), + 'min': (np.min, 'Minimum value', 'min(PIKA)'), + 'argmax': (np.argmax, 'Index of maximum value', 'argmax(PIKA)'), + 'argmin': (np.argmax, 'Index of minimum value', 'argmin(PIKA)'), + }, parents=('Basic', 'Values')), if const: self.add_namespace( - {'e': (constants.e, 'e / As'), - 'eps0': (constants.epsilon0, 'epsilon0 / As/Vm'), - 'Eu': (constants.Eu,), 'h': (constants.h, 'h / eVs'), - 'hbar': (constants.hbar, 'hbar / eVs'), 'kB': (constants.kB, 'kB / eV/K'), - 'mu0': (constants.mu0, 'mu0 / Vs/Am'), 'NA': (constants.NA, 'NA / 1/mol'), - 'pi': (constants.pi,), 'R': (constants.R, 'R / eV'), - }, + { + 'e': (constants.e, 'e / As'), + 'eps0': (constants.epsilon0, 'epsilon0 / As/Vm'), + 'Eu': (constants.Eu,), + 'h': (constants.h, 'h / eVs'), + 'hbar': (constants.hbar, 'hbar / eVs'), + 'kB': (constants.kB, 'kB / eV/K'), + 'mu0': (constants.mu0, 'mu0 / Vs/Am'), + 'NA': (constants.NA, 'NA / 1/mol'), + 'pi': (constants.pi,), + 'R': (constants.R, 'R / eV'), + }, parents=('Constants', 'Maybe useful'), ) self.add_namespace( {f'gamma["{k}"]': (v, k, f'gamma["{k}"]') for k, v in constants.gamma.items()}, - parents=('Constants', 'Magnetogyric ratios (in 1/(sT))') + parents=('Constants', 'Gyromagnetic ratios (in 1/(sT))') ) if fitfuncs: @@ -199,7 +207,7 @@ class QNamespaceWidget(QtWidgets.QWidget, Ui_Form): for entry in subspace: key_item = QtWidgets.QTableWidgetItem(entry) - key_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + key_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) vals = self.namespace.namespace[entry] @@ -214,12 +222,12 @@ class QNamespaceWidget(QtWidgets.QWidget, Ui_Form): display = vals[1] value_item = QtWidgets.QTableWidgetItem(display) - value_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + value_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) - key_item.setData(QtCore.Qt.UserRole, alias) - key_item.setData(QtCore.Qt.UserRole+1, entry) - value_item.setData(QtCore.Qt.UserRole, alias) - value_item.setData(QtCore.Qt.UserRole+1, entry) + key_item.setData(QtCore.Qt.ItemDataRole.UserRole, alias) + key_item.setData(QtCore.Qt.ItemDataRole.UserRole+1, entry) + value_item.setData(QtCore.Qt.ItemDataRole.UserRole, alias) + value_item.setData(QtCore.Qt.ItemDataRole.UserRole+1, entry) row = self.namespace_table.rowCount() self.namespace_table.setRowCount(row+1) @@ -241,5 +249,5 @@ class QNamespaceWidget(QtWidgets.QWidget, Ui_Form): @QtCore.pyqtSlot(QtWidgets.QTableWidgetItem, name='on_namespace_table_itemDoubleClicked') def item_selected(self, item: QtWidgets.QTableWidgetItem): - self.selected.emit(item.data(QtCore.Qt.UserRole)) - self.sendKey.emit(item.data(QtCore.Qt.UserRole+1)) + self.selected.emit(item.data(QtCore.Qt.ItemDataRole.UserRole)) + self.sendKey.emit(item.data(QtCore.Qt.ItemDataRole.UserRole+1)) diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 39fdbea..8575319 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -2,7 +2,6 @@ from __future__ import annotations import os import re -import time from pathlib import Path from numpy import geomspace, linspace @@ -17,13 +16,13 @@ from ..Qt import QtGui, QtPrintSupport from ..data.shift_graphs import QShift from ..data.signaledit import QPreviewDialog, QBaselineDialog from ..dsc.glass_dialog import TgCalculator +from ..fit.fit_toolbar import FitToolbar from ..fit.result import FitExtension, QFitResult from ..graphs.graphwindow import QGraphWindow from ..graphs.movedialog import QMover from ..io.fcbatchreader import QFCReader from ..io.filedialog import * from ..lib.iconloading import make_action_icons, get_icon -from ..lib.pg_objects import RegionItem from ..lib.starter import make_starter from ..math.binning import BinningWindow from ..math.evaluation import QEvalDialog @@ -97,12 +96,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.norm_toolbutton.setIcon(get_icon('normal')) self.toolbar_edit.addWidget(self.norm_toolbutton) - self.fitlim_button = QtWidgets.QToolButton(self) - self.fitlim_button.setMenu(self.menuLimits) - self.fitlim_button.setPopupMode(self.fitlim_button.InstantPopup) - self.fitlim_button.setIcon(get_icon('fit_region')) - self.toolBar_fit.addWidget(self.fitlim_button) - while self.tabWidget.count() > 2: self.tabWidget.removeTab(self.tabWidget.count()-1) @@ -120,9 +113,11 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): # noinspection PyUnresolvedReferences self.statusBar.addWidget(self.mousepos) - self.fitregion = RegionItem() self._fit_plot_id = None + self.fit_toolbar = FitToolbar(self.action_FitWidget, self.menuLimits, self) + self.addToolBar(self.fit_toolbar) + self.setGeometry(QtWidgets.QStyle.alignedRect( QtCore.Qt.LayoutDirection.LeftToRight, QtCore.Qt.AlignmentFlag.AlignCenter, @@ -138,11 +133,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.ac_group.addAction(self.action_nm_fit) self.ac_group.addAction(self.action_odr_fit) - self.ac_group2 = QtWidgets.QActionGroup(self) - self.ac_group2.addAction(self.action_no_range) - self.ac_group2.addAction(self.action_x_range) - self.ac_group2.addAction(self.action_custom_range) - def _init_signals(self): self.actionRedo = self.management.undostack.createRedoAction(self) icon = QtGui.QIcon.fromTheme("edit-redo") @@ -158,7 +148,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.action_save_fit_parameter.triggered.connect(self.save_fit_parameter) # noinspection PyUnresolvedReferences - self.ac_group2.triggered.connect(self.change_fit_limits) + self.fit_toolbar.limit_group.triggered.connect(self.change_fit_limits) self.t1action.triggered.connect(lambda: self._show_tab('t1_temp')) self.action_edit.triggered.connect(self.do_preview) @@ -899,29 +889,28 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): for item in self.fit_dialog.preview_lines: self.current_graph_widget.add_external(item) if self.action_custom_range.isChecked(): - self.current_graph_widget.add_external(self.fitregion) + self.current_graph_widget.add_external(self.fit_toolbar.region) block_window = True else: for item in self.fit_dialog.preview_lines: self.current_graph_widget.remove_external(item) - self.current_graph_widget.remove_external(self.fitregion) + self.current_graph_widget.remove_external(self.fit_toolbar.region) return block_window @QtCore.pyqtSlot(QtWidgets.QAction) def change_fit_limits(self, action: QtWidgets.QAction): + if self.current_graph_widget is None: + return + if action == self.action_custom_range and self.fit_dialog.isVisible(): - self.current_graph_widget.add_external(self.fitregion) + self.current_graph_widget.add_external(self.fit_toolbar.region) else: - self.current_graph_widget.remove_external(self.fitregion) + self.current_graph_widget.remove_external(self.fit_toolbar.region) def start_fit(self, parameter, links, fit_options): - fit_options['limits'] = { - self.action_no_range: 'none', - self.action_x_range: 'x', - self.action_custom_range: self.fitregion.getRegion() - }[self.ac_group2.checkedAction()] + fit_options['limits'] = self.fit_toolbar.get_limit() fit_options['fit_mode'] = { self.action_lm_fit: 'lsq', diff --git a/src/resources/_ui/basewindow.ui b/src/resources/_ui/basewindow.ui index 8a5efaf..bf3de79 100644 --- a/src/resources/_ui/basewindow.ui +++ b/src/resources/_ui/basewindow.ui @@ -437,30 +437,6 @@ - - - - 0 - 0 - - - - Fit - - - - 24 - 24 - - - - TopToolBarArea - - - false - - - diff --git a/src/resources/_ui/fitmodelwidget.ui b/src/resources/_ui/fitmodelwidget.ui index ffc0b93..d36a6b9 100755 --- a/src/resources/_ui/fitmodelwidget.ui +++ b/src/resources/_ui/fitmodelwidget.ui @@ -67,6 +67,12 @@ 0 + + + 160 + 16777215 + + Initial values @@ -92,6 +98,13 @@ + + + + Use global + + + @@ -151,6 +164,12 @@ 0 + + + 100 + 16777215 + + <html><head/><body><p>Lower bound. Same bound is used for all data. Leave empty for no boundary condition.</p></body></html> @@ -195,6 +214,12 @@ 0 + + + 100 + 16777215 + + <html><head/><body><p>Upper bound. Same bound is used for all data. Leave empty for no boundary condition.</p></body></html>