diff --git a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py index d93c50f218..7cbb99f2a5 100644 --- a/src/sas/qtgui/Calculators/GenericScatteringCalculator.py +++ b/src/sas/qtgui/Calculators/GenericScatteringCalculator.py @@ -25,7 +25,7 @@ import periodictable import sas.qtgui.Utilities.GuiUtils as GuiUtils -from sas.qtgui.Utilities.TabbedModelEditor import TabbedModelEditor +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.TabbedModelEditor import TabbedModelEditor from sas.qtgui.Utilities.GenericReader import GenReader from sasdata.dataloader.data_info import Detector, Source from sas.system.version import __version__ diff --git a/src/sas/qtgui/MainWindow/GuiManager.py b/src/sas/qtgui/MainWindow/GuiManager.py index fe3c14ca81..77c45db56e 100644 --- a/src/sas/qtgui/MainWindow/GuiManager.py +++ b/src/sas/qtgui/MainWindow/GuiManager.py @@ -28,8 +28,9 @@ import sas.qtgui.Utilities.GuiUtils as GuiUtils import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary -from sas.qtgui.Utilities.TabbedModelEditor import TabbedModelEditor +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.TabbedModelEditor import TabbedModelEditor from sas.qtgui.Utilities.PluginManager import PluginManager +from sas.qtgui.Utilities.ModelEditors.ReparamEditor.ReparameterizationEditor import ReparameterizationEditor from sas.qtgui.Utilities.GridPanel import BatchOutputPanel from sas.qtgui.Utilities.ResultPanel import ResultPanel from sas.qtgui.Utilities.OrientationViewer.OrientationViewer import show_orientation_viewer @@ -68,7 +69,7 @@ from sas.qtgui.MainWindow.DataExplorer import DataExplorerWindow -from sas.qtgui.Utilities.AddMultEditor import AddMultEditor +from sas.qtgui.Utilities.ModelEditors.AddMultEditor.AddMultEditor import AddMultEditor from sas.qtgui.Utilities.ImageViewer import ImageViewer from sas.qtgui.Utilities.FileConverter import FileConverterWidget from sas.qtgui.Utilities.WhatsNew.WhatsNew import WhatsNew @@ -755,6 +756,7 @@ def addTriggers(self): self._workspace.actionAdd_Custom_Model.triggered.connect(self.actionAdd_Custom_Model) self._workspace.actionEdit_Custom_Model.triggered.connect(self.actionEdit_Custom_Model) self._workspace.actionManage_Custom_Models.triggered.connect(self.actionManage_Custom_Models) + self._workspace.actionReparameterize_Model.triggered.connect(self.actionReparameterize_Model) self._workspace.actionAddMult_Models.triggered.connect(self.actionAddMult_Models) self._workspace.actionEditMask.triggered.connect(self.actionEditMask) @@ -1169,6 +1171,12 @@ def actionManage_Custom_Models(self): self.model_manager = PluginManager(self) self.model_manager.show() + def actionReparameterize_Model(self): + """ + """ + self.reparameterizer = ReparameterizationEditor(self) + self.reparameterizer.show() + def actionAddMult_Models(self): """ """ diff --git a/src/sas/qtgui/Utilities/CodeEditor.py b/src/sas/qtgui/Utilities/CustomGUI/CodeEditor.py similarity index 100% rename from src/sas/qtgui/Utilities/CodeEditor.py rename to src/sas/qtgui/Utilities/CustomGUI/CodeEditor.py diff --git a/src/sas/qtgui/Utilities/CustomGUI/ParameterTree.py b/src/sas/qtgui/Utilities/CustomGUI/ParameterTree.py new file mode 100644 index 0000000000..779c655ee2 --- /dev/null +++ b/src/sas/qtgui/Utilities/CustomGUI/ParameterTree.py @@ -0,0 +1,25 @@ +from PySide6.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget +from PySide6.QtGui import QPainter, QPen, QColor, QFont +from PySide6.QtCore import Qt, QRect + +class QParameterTreeWidget(QTreeWidget): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.disabled_text = "" + + def setDisabledText(self, text): + self.disabled_text = text + self.viewport().update() + + def paintEvent(self, event): + super().paintEvent(event) + + if not self.isEnabled() and self.disabled_text: + painter = QPainter(self.viewport()) + painter.setRenderHint(QPainter.Antialiasing) + painter.setRenderHint(QPainter.TextAntialiasing) + painter.setPen(QPen(QColor(170, 170, 170))) # Light gray color for text + painter.setFont(QFont("Segoe UI", 11, italic=True)) + + rect = self.viewport().rect() + painter.drawText(rect, Qt.AlignCenter, self.disabled_text) \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/DocViewWidget.py b/src/sas/qtgui/Utilities/DocViewWidget.py index 7b6e84e8bc..d59900b7c1 100644 --- a/src/sas/qtgui/Utilities/DocViewWidget.py +++ b/src/sas/qtgui/Utilities/DocViewWidget.py @@ -8,7 +8,7 @@ from twisted.internet import threads from .UI.DocViewWidgetUI import Ui_DocViewerWindow -from sas.qtgui.Utilities.TabbedModelEditor import TabbedModelEditor +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.TabbedModelEditor import TabbedModelEditor from sas.sascalc.fit import models from sas.sascalc.data_util.calcthread import CalcThread from sas.sascalc.doc_regen.makedocumentation import (make_documentation, create_user_files_if_needed, diff --git a/src/sas/qtgui/Utilities/AddMultEditor.py b/src/sas/qtgui/Utilities/ModelEditors/AddMultEditor/AddMultEditor.py similarity index 99% rename from src/sas/qtgui/Utilities/AddMultEditor.py rename to src/sas/qtgui/Utilities/ModelEditors/AddMultEditor/AddMultEditor.py index 8ba56c3d0e..83af7bd136 100644 --- a/src/sas/qtgui/Utilities/AddMultEditor.py +++ b/src/sas/qtgui/Utilities/ModelEditors/AddMultEditor/AddMultEditor.py @@ -23,7 +23,7 @@ from sas.qtgui.Perspectives.Fitting.FittingWidget import SUPPRESSED_MODELS # Local UI -from sas.qtgui.Utilities.UI.AddMultEditorUI import Ui_AddMultEditorUI +from sas.qtgui.Utilities.ModelEditors.AddMultEditor.UI.AddMultEditorUI import Ui_AddMultEditorUI # Template for the output plugin file SUM_TEMPLATE = """ diff --git a/src/sas/qtgui/Utilities/UI/AddMultEditorUI.ui b/src/sas/qtgui/Utilities/ModelEditors/AddMultEditor/UI/AddMultEditorUI.ui similarity index 96% rename from src/sas/qtgui/Utilities/UI/AddMultEditorUI.ui rename to src/sas/qtgui/Utilities/ModelEditors/AddMultEditor/UI/AddMultEditorUI.ui index bc1a331fdb..19c09d47fe 100644 --- a/src/sas/qtgui/Utilities/UI/AddMultEditorUI.ui +++ b/src/sas/qtgui/Utilities/ModelEditors/AddMultEditor/UI/AddMultEditorUI.ui @@ -1,187 +1,187 @@ - - - AddMultEditorUI - - - - 0 - 0 - 527 - 331 - - - - - 0 - 0 - - - - - 527 - 331 - - - - Easy Add/Multiply Editor - - - - - - Description - - - - - - Enter a description of the model (optional) - - - - - - - - - - Plugin model - - - - - - Sum / Multiply model function name. - - - Enter a plugin name - - - - - - - Check to overwrite the existing model with the same name. - - - Overwrite existing model - - - - - - - - - - Model selection - - - - - - - - model_1 - - - - - - - model_2 - - - - - - - true - - - - - - - - 0 - 0 - - - - Add: + -Multiply: * - - - - + - - - - - * - - - - - @ - - - - - - - - true - - - - - - - - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - QDialogButtonBox::Apply|QDialogButtonBox::Close|QDialogButtonBox::Help - - - - - - - - - <html><head/><body><p><span style=" font-weight:600;">Plugin_model = scale_factor * (model_1 + model_2) + background</span></p><p>To add/multiply plugin models, or combine more than two models, please click Help below.<br/></p></body></html> - - - - - - - txtName - chkOverwrite - txtDescription - cbModel1 - cbOperator - cbModel2 - - - - + + + AddMultEditorUI + + + + 0 + 0 + 527 + 331 + + + + + 0 + 0 + + + + + 527 + 331 + + + + Easy Add/Multiply Editor + + + + + + Description + + + + + + Enter a description of the model (optional) + + + + + + + + + + Plugin model + + + + + + Sum / Multiply model function name. + + + Enter a plugin name + + + + + + + Check to overwrite the existing model with the same name. + + + Overwrite existing model + + + + + + + + + + Model selection + + + + + + + + model_1 + + + + + + + model_2 + + + + + + + true + + + + + + + + 0 + 0 + + + + Add: + +Multiply: * + + + + + + + + + + * + + + + + @ + + + + + + + + true + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + QDialogButtonBox::Apply|QDialogButtonBox::Close|QDialogButtonBox::Help + + + + + + + + + <html><head/><body><p><span style=" font-weight:600;">Plugin_model = scale_factor * (model_1 + model_2) + background</span></p><p>To add/multiply plugin models, or combine more than two models, please click Help below.<br/></p></body></html> + + + + + + + txtName + chkOverwrite + txtDescription + cbModel1 + cbOperator + cbModel2 + + + + diff --git a/src/sas/qtgui/Utilities/ModelEditors/Dialogs/ModelSelector.py b/src/sas/qtgui/Utilities/ModelEditors/Dialogs/ModelSelector.py new file mode 100644 index 0000000000..12bd7d1414 --- /dev/null +++ b/src/sas/qtgui/Utilities/ModelEditors/Dialogs/ModelSelector.py @@ -0,0 +1,217 @@ +import json +import logging +import os.path +from collections import defaultdict + +from PySide6 import QtWidgets, QtCore, QtGui + +from sasmodels import generate +from sasmodels import modelinfo +from sasmodels.sasview_model import load_standard_models + +from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller +from sas.qtgui.Utilities.ModelEditors.Dialogs.UI.ModelSelectorUI import Ui_ModelSelector + +from sas.sascalc.fit import models + +logger = logging.getLogger(__name__) + +CATEGORY_CUSTOM = "Plugin Models" +CATEGORY_STRUCTURE = "Structure Factor" + +class ModelSelector(QtWidgets.QDialog, Ui_ModelSelector): + """ + Helper widget to get model parameters from a list of available models sorted by type + """ + + # Signals + returnModelParamsSignal = QtCore.Signal(str, list) + + def __init__(self, parent=None): + super(ModelSelector, self).__init__(parent) + self.setupUi(self) + + self.parent = parent + self.models = {} + self.custom_models = self.customModels() + self.selection = None + self.model_parameters = None + + self.addSignals() + self.onLoad() + + def addSignals(self): + """ + Connect signals to slots + """ + if self.parent: + self.parent.destroyed.connect(self.onClose) + self.modelTree.itemSelectionChanged.connect(self.onSelectionChanged) + self.cmdLoadModel.clicked.connect(self.onLoadModel) + self.cmdCancel.clicked.connect(self.onClose) + + def onLoad(self): + # Create model dictionary of all models and load it into the QTreeWidget + self.setupModelDict() + self.populateModelTree() + + def setupModelDict(self): + """ + Setup a dictionary of all available models and their categories + """ + self.master_category_dict = defaultdict(list) + self.by_model_dict = defaultdict(list) + self.model_enabled_dict = defaultdict(bool) + + categorization_file = CategoryInstaller.get_user_file() + if not os.path.isfile(categorization_file): + categorization_file = CategoryInstaller.get_default_file() + with open(categorization_file, 'rb') as cat_file: + self.master_category_dict = json.load(cat_file) + self.regenerateModelDict() + + # Load list of available models + models = load_standard_models() + for model in models: + self.models[model.name] = model + + self.readCustomCategoryInfo() + + def readCustomCategoryInfo(self): + """ + Reads the custom model category + """ + #Looking for plugins + self.plugins = list(self.custom_models.values()) + plugin_list = [] + for name, plug in self.custom_models.items(): + self.models[name] = plug + plugin_list.append([name, True]) + if plugin_list: + self.master_category_dict[CATEGORY_CUSTOM] = plugin_list + # Adding plugins classified as structure factor to 'CATEGORY_STRUCTURE' list + if CATEGORY_STRUCTURE in self.master_category_dict: + plugin_structure_list = [ + [name, True] for name, plug in self.custom_models.items() + if plug.is_structure_factor + and [name, True] not in self.master_category_dict[CATEGORY_STRUCTURE] + ] + if plugin_structure_list: + self.master_category_dict[CATEGORY_STRUCTURE].extend(plugin_structure_list) + + def regenerateModelDict(self): + """ + Regenerates self.by_model_dict which has each model name as the + key and the list of categories belonging to that model + along with the enabled mapping + """ + self.by_model_dict = defaultdict(list) + for category in self.master_category_dict: + for (model, enabled) in self.master_category_dict[category]: + self.by_model_dict[model].append(category) + self.model_enabled_dict[model] = enabled + + def populateModelTree(self): + """ + Populate the model tree with available models + """ + for category in self.master_category_dict: + category_item = QtWidgets.QTreeWidgetItem(self.modelTree) + category_item.setText(0, category) + for (model, _) in self.master_category_dict[category]: + model_item = QtWidgets.QTreeWidgetItem(category_item) + model_item.setText(0, model) + + def onSelectionChanged(self): + """ + Update selected model and display user selection + """ + # Only one item can be selected at a time as per selectionMode = SingleSelection in the .ui file + selected_items = self.modelTree.selectedItems() + if selected_items: + selected_item = selected_items[0] + if selected_item.parent() is None: + # User selected a category. Remove selection. + self.modelTree.blockSignals(True) + self.modelTree.clearSelection() + self.modelTree.blockSignals(False) + else: + # User selected a model. Display selection in label. + self.selection = selected_item.text(0) + self.lblSelection.clear() + self.lblSelection.setText(self.selection) + + def onLoadModel(self): + """ + Get parameters for selected model, convert to usable data, send to parent. Close dialog if sucessful. + """ + iq_parameters = self.getParameters() + self.returnModelParamsSignal.emit(self.lblSelection.text(), iq_parameters) + self.close() + self.deleteLater() + + def getParameters(self): + """ + Get parameters for the selected model and return them as a list + """ + name = self.selection + kernel_module = None + + if self.modelTree.selectedItems()[0].parent() == CATEGORY_CUSTOM: + # custom kernel load requires full path + name = os.path.join(models.find_plugins_dir(), name+".py") + try: + kernel_module = generate.load_kernel_module(name) + except ModuleNotFoundError as ex: + pass + except FileNotFoundError as ex: + # can happen when name attribute not the same as actual filename + pass + + if kernel_module is None: + # mismatch between "name" attribute and actual filename. + curr_model = self.models[self.selection] + name, _ = os.path.splitext(os.path.basename(curr_model.filename)) + try: + kernel_module = generate.load_kernel_module(name) + except ModuleNotFoundError as ex: + logger.error("Can't find the model "+ str(ex)) + return + + if hasattr(kernel_module, 'model_info'): + # for sum/multiply models + self.model_parameters = kernel_module.model_info.parameters + + elif hasattr(kernel_module, 'parameters'): + # built-in and custom models + self.model_parameters = modelinfo.make_parameter_table(getattr(kernel_module, 'parameters', [])) + + elif hasattr(kernel_module, 'model_info'): + # for sum/multiply models + self.model_parameters = kernel_module.model_info.parameters + + elif hasattr(kernel_module, 'Model') and hasattr(kernel_module.Model, "_model_info"): + # this probably won't work if there's no model_info, but just in case + self.model_parameters = kernel_module.Model._model_info.parameters + else: + # no parameters - default to blank table + msg = "No parameters found in model '{}'.".format(self.selection) + logger.warning(msg) + self.model_parameters = modelinfo.ParameterTable([]) + + return self.model_parameters.iq_parameters + + @classmethod + def customModels(cls): + """ + Read in file names in the custom plugin directory + """ + manager = models.ModelManager() + # TODO: Cache plugin models instead of scanning the directory each time. + manager.update() + # TODO: Define plugin_models property in ModelManager. + return manager.base.plugin_models + + def onClose(self): + self.close() + self.deleteLater() \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/ModelEditors/Dialogs/ParameterEditDialog.py b/src/sas/qtgui/Utilities/ModelEditors/Dialogs/ParameterEditDialog.py new file mode 100644 index 0000000000..627f0def3e --- /dev/null +++ b/src/sas/qtgui/Utilities/ModelEditors/Dialogs/ParameterEditDialog.py @@ -0,0 +1,146 @@ +import logging +from numpy import inf + +from PySide6 import QtWidgets, QtCore, QtGui + +from sas.qtgui.Utilities.ModelEditors.Dialogs.UI.ParameterEditDialogUI import Ui_ParameterEditDialog + +from sasmodels.modelinfo import Parameter + +logger = logging.getLogger(__name__) + +class ParameterEditDialog(QtWidgets.QDialog, Ui_ParameterEditDialog): + + # Signals + returnNewParamsSignal = QtCore.Signal(list) + returnEditedParamSignal = QtCore.Signal(list, QtWidgets.QTreeWidgetItem) + + def __init__(self, parent=None, properties=None, qtree_item=None): + super(ParameterEditDialog, self).__init__(parent) + + self.parent = parent + self.properties = properties + self.qtree_item = qtree_item + + self.setupUi(self) + + self.addSignals() + + self.onLoad() + + def addSignals(self): + if self.parent: + self.parent.destroyed.connect(self.onClose) + self.valuesTable.cellPressed.connect(self.onCellPressed) + self.cmdCancel.clicked.connect(self.onClose) + self.cmdSave.clicked.connect(self.onSave) + + def onLoad(self): + self.valuesTable.resizeRowsToContents() + self.adjustTableSize() + + # Detect if properties are passed in (if we are in edit-mode) + if self.properties: + self.setWindowTitle("Edit Parameter: %s" % self.properties['name']) + + # Load properties into table + for property in self.properties: + if property not in ("name", "highlighted_property", "id"): + self.writeValuesToTable(self.valuesTable, property, str(self.properties[property])) + elif property == "name": + self.txtName.setText(self.properties[property]) + elif property == "id": + self.id = self.properties[property] + + def onCellPressed(self): + # Clear bold formatting in the first column + for row in range(self.valuesTable.rowCount()): + item = self.valuesTable.item(row, 0) + font = item.font() + font.setBold(False) + item.setFont(font) + + # Make text bolded in the clicked-on box in the first column + selected_row = self.valuesTable.currentRow() + item = self.valuesTable.item(selected_row, 0) + font = item.font() + font.setBold(True) + item.setFont(font) + + def onSave(self): + """ + Return the values in the table to the listening parent widget + """ + self.cmdSave.setFocus() # Ensure that all table values are written to table's data() before saving + + if self.properties: + self.returnEditedParamSignal.emit(self.getValues(), self.qtree_item) + else: + self.returnNewParamsSignal.emit(self.getValues()) + + self.onClose() + + def getValues(self): + """ + Get the values from the table and return them as a parameter object + """ + parameter = Parameter(name=self.txtName.text()) + minimum_value = self.valuesTable.item(self.valuesTable.findItems("Min", QtCore.Qt.MatchContains)[0].row(), 1).text() + maximum_value = self.valuesTable.item(self.valuesTable.findItems("Max", QtCore.Qt.MatchContains)[0].row(), 1).text() + + # Format minimum and maximum values into required format for Parameter object tuple(float, float) + limits = [minimum_value, maximum_value] + for i in range(len(limits)): + try: + limits[i] = float(limits[i]) + except ValueError: + if "inf" in limits[i]: + limits[i] = inf + if "-" in limits[i]: + limits[i] = -inf + else: + logger.error("Invalid limit value: %s" % limits[i]) + return None + parameter.limits = tuple(limits) + parameter.default = self.getValuesFromTable(self.valuesTable, "Default") + parameter.units = self.getValuesFromTable(self.valuesTable, "Units") + parameter.type = self.getValuesFromTable(self.valuesTable, "Type") + parameter.description = self.getValuesFromTable(self.valuesTable, "Description") + + return [parameter] + + def adjustTableSize(self): + self.valuesTable.setFixedHeight(self.valuesTable.verticalHeader().length() + self.valuesTable.horizontalHeader().height()) + + @staticmethod + def getValuesFromTable(table, search_string): + """ + Get values from column 2 of table given a search string in column 1 + :param table: QTableWidget + :param search_string: str + """ + property_row = table.findItems(search_string, QtCore.Qt.MatchContains)[0].row() + try: + return table.item(property_row, 1).text() + except AttributeError: + return "" + + @staticmethod + def writeValuesToTable(table, search_string, value): + """ + Write values to column 2 of table given a search string in column 1 + :param table: QTableWidget + :param search_string: str + """ + property_row = table.findItems(search_string, QtCore.Qt.MatchContains)[0].row() + try: + return table.item(property_row, 1).setText(value) + except AttributeError: + # Generate and place a blank QTableWidgetItem so we can set its text + new_item = QtWidgets.QTableWidgetItem() + table.setItem(property_row, 1, new_item) + return table.item(property_row, 1).setText(value) + + def onClose(self): + self.close() + self.deleteLater() \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/ModelEditors/Dialogs/UI/ModelSelectorUI.ui b/src/sas/qtgui/Utilities/ModelEditors/Dialogs/UI/ModelSelectorUI.ui new file mode 100644 index 0000000000..bf6ae65d3a --- /dev/null +++ b/src/sas/qtgui/Utilities/ModelEditors/Dialogs/UI/ModelSelectorUI.ui @@ -0,0 +1,119 @@ + + + ModelSelector + + + + 0 + 0 + 420 + 300 + + + + + 0 + 0 + + + + + 420 + 16777215 + + + + Select Model + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + false + + + + 1 + + + + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 75 + 0 + + + + Cancel + + + + + + + + 100 + 0 + + + + Load Model + + + + + + + + + + + + + diff --git a/src/sas/qtgui/Utilities/ModelEditors/Dialogs/UI/ParameterEditDialogUI.ui b/src/sas/qtgui/Utilities/ModelEditors/Dialogs/UI/ParameterEditDialogUI.ui new file mode 100644 index 0000000000..4512ea3a8b --- /dev/null +++ b/src/sas/qtgui/Utilities/ModelEditors/Dialogs/UI/ParameterEditDialogUI.ui @@ -0,0 +1,272 @@ + + + ParameterEditDialog + + + + 0 + 0 + 400 + 300 + + + + + 0 + 0 + + + + + 16777215 + 300 + + + + New Parameter + + + + + + Parameter Definition + + + + + + + + Parameter Name + + + + + + + + 0 + 0 + + + + + + + + + + + + + + 0 + 0 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 0 + 0 + + + + QAbstractItemView::NoSelection + + + QAbstractItemView::SelectRows + + + Qt::ElideNone + + + false + + + 6 + + + 2 + + + false + + + true + + + true + + + false + + + false + + + false + + + + + + + + + + + + Default Value + + + AlignCenter + + + ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable|ItemIsEnabled + + + + + 0 + + + + + Minimum + + + AlignCenter + + + ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable|ItemIsEnabled + + + + + -inf + + + + + Maximum + + + AlignCenter + + + ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable|ItemIsEnabled + + + + + inf + + + + + Units + + + + false + + + + AlignCenter + + + ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable|ItemIsEnabled + + + + + + + + + + Type + + + + false + + + + AlignCenter + + + ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable|ItemIsEnabled + + + + + + + + + + Description + + + + false + + + + AlignCenter + + + ItemIsSelectable|ItemIsDragEnabled|ItemIsDropEnabled|ItemIsUserCheckable|ItemIsEnabled + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + Save + + + + + + + + + + diff --git a/src/sas/qtgui/Utilities/ModelEditors/ReparamEditor/ReparameterizationEditor.py b/src/sas/qtgui/Utilities/ModelEditors/ReparamEditor/ReparameterizationEditor.py new file mode 100644 index 0000000000..1f42a7933f --- /dev/null +++ b/src/sas/qtgui/Utilities/ModelEditors/ReparamEditor/ReparameterizationEditor.py @@ -0,0 +1,693 @@ +import ast +import logging +import os +import pathlib +import re +import traceback + +from numpy import inf +from PySide6 import QtWidgets, QtCore, QtGui + +from sas.sascalc.fit.models import find_plugins_dir + +from sas.qtgui.Utilities import GuiUtils +from sas.qtgui.Utilities.ModelEditors.ReparamEditor.UI.ReparameterizationEditorUI import Ui_ReparameterizationEditor +from sas.qtgui.Utilities.ModelEditors.Dialogs.ModelSelector import ModelSelector +from sas.qtgui.Utilities.ModelEditors.Dialogs.ParameterEditDialog import ParameterEditDialog + +from sasmodels.modelinfo import Parameter + +logger = logging.getLogger(__name__) + +class ReparameterizationEditor(QtWidgets.QDialog, Ui_ReparameterizationEditor): + + def __init__(self, parent=None): + super(ReparameterizationEditor, self).__init__(parent._parent) + + self.parent = parent + + self.setupUi(self) + + self.addSignals() + + self.onLoad() + + self.is_modified = False + self.loaded_model_name = None # Name of model loaded into oldParamTree + + def addSignals(self): + self.selectModelButton.clicked.connect(self.onSelectModel) + self.cmdCancel.clicked.connect(self.close) + self.cmdApply.clicked.connect(self.onApply) + self.cmdAddParam.clicked.connect(self.onAddParam) + self.cmdDeleteParam.clicked.connect(self.onDeleteParam) + self.cmdEditSelected.clicked.connect(self.editSelected) + self.txtNewModelName.textChanged.connect(self.editorModelModified) + self.txtFunction.textChanged.connect(self.editorModelModified) + self.newParamTree.doubleClicked.connect(self.editSelected) + self.cmdHelp.clicked.connect(self.onHelp) + self.cmdModelHelp.clicked.connect(self.onModelHelp) + + def onLoad(self): + + # Disable `Apply` button + self.cmdApply.setEnabled(False) + + self.oldParamTree.setDisabledText("Load a model to display") + self.newParamTree.setDisabledText("Add a parameter to display") + self.oldParamTree.setEnabled(False) + self.newParamTree.setEnabled(False) + + self.addTooltips() + + #TODO: Set the default text for the function editor + text = \ +"""""" + self.txtFunction.insertPlainText(text) + self.txtFunction.setFont(GuiUtils.getMonospaceFont()) + + # Validators + rx = QtCore.QRegularExpression("^[A-Za-z0-9_]*$") + + txt_validator = QtGui.QRegularExpressionValidator(rx) + self.txtNewModelName.setValidator(txt_validator) + # Weird import location - workaround for a bug in Sphinx choking on + # importing QSyntaxHighlighter + # DO NOT MOVE TO TOP + from sas.qtgui.Utilities.PythonSyntax import PythonHighlighter + self.highlight = PythonHighlighter(self.txtFunction.document()) + + def addTooltips(self): + """ + Add the default tooltips to the text field + """ + hint_function = "Example:\n\n" + hint_function += "helper_constant = new_parameter1 * M_PI\n" + hint_function += "old_parameter1 = helper_constant\n" + hint_function += "old_parameter2 = helper_constant / new_parameter2\n" + self.txtFunction.setToolTip(hint_function) + + def onSelectModel(self): + """ + Launch model selection dialog + """ + self.model_selector = ModelSelector(self) + self.model_selector.returnModelParamsSignal.connect(lambda model_name, params: self.loadParams(params, self.oldParamTree, model_name)) + self.model_selector.show() + + def loadParams(self, params, tree, model_name=None): + """ + Load parameters from the selected model into a tree widget + :param param: sasmodels.modelinfo.Parameter class + :param tree: the tree widget to load the parameters into + :param model_name: the name of the model that the parameters are from + """ + if tree == self.oldParamTree: + tree.clear() # Clear the tree widget + self.loaded_model_name = model_name + + if not tree.isEnabled(): + # Enable tree if necessary + tree.setEnabled(True) + + # Add parameters to the tree + for param in params: + item = QtWidgets.QTreeWidgetItem(tree) + item.setText(0, param.name) + tree.addTopLevelItem(item) + self.addSubItems(param, item) + self.badPropsCheck(item) + + if tree == self.oldParamTree: + if tree.topLevelItemCount() == 0: + # If no parameters were found, disable the tree + tree.setDisabledText("No parameters found in model") + tree.setEnabled(False) + # Once model is loaded sucessfully, update txtSelectModelInfo to reflect the model name + self.old_model_name = model_name + self.lblSelectModelInfo.setText("Model %s loaded successfully" % self.old_model_name) + self.selectModelButton.setText("Change...") + + # Resize all columns to fit loaded content + for i in range(tree.columnCount()): + tree.resizeColumnToContents(i) + + self.editorModelModified() + + # Check for duplicate parameter names + self.checkDuplicates(tree) + + def onAddParam(self): + """ + Add parameter to "New parameters box" by invoking parameter editor dialog + """ + self.param_creator = ParameterEditDialog(self) + self.param_creator.returnNewParamsSignal.connect(lambda params: self.loadParams(params, self.newParamTree)) + self.param_creator.show() + + def onDeleteParam(self): + """ + Delete the selected parameter from the newParamTree + """ + delete_sucessful = False # Track whether the delete action was sucessful or not + + # Get selected item + selected_item = self.newParamTree.currentItem() + param_to_delete = self.getParameterSelection(selected_item) + + # Find the parameter item by using param_to_delete + for i in range(self.newParamTree.topLevelItemCount()): + param = self.newParamTree.topLevelItem(i) + if param == param_to_delete: + # Remove the parameter from the tree + self.newParamTree.takeTopLevelItem(i) + delete_sucessful = True + + if self.newParamTree.topLevelItemCount() == 0: + # If there are no parameters left, disable the tree + self.newParamTree.setEnabled(False) + + if not delete_sucessful: + return logger.warning("Could not find parameter to delete: %s" % param_to_delete.text(0)) + else: + self.editorModelModified() + + def editSelected(self): + """ + Edit the selected parameter in a new parameter editor dialog + """ + # Get selected item + selected_item = self.newParamTree.currentItem() # The item that the user selected (could be a sub-item) + if not selected_item: + # User has no current selection in newParamTree. Throw a warning dialog and return. + msg = "No parameter selected. Please select a parameter to edit." + msgBox = QtWidgets.QMessageBox(self) + msgBox.setIcon(QtWidgets.QMessageBox.Warning) + msgBox.setWindowTitle("No Parameter Selected") + msgBox.setText(msg) + msgBox.addButton("OK", QtWidgets.QMessageBox.AcceptRole) + return msgBox.exec_() + + param_to_open = self.getParameterSelection(selected_item) # The top-level item to open + + highlighted_property = selected_item.text(0) # What the user wants to edit specifically + + # Find the parameter item by using param_to_open and format as a dictionary + param_properties = self.getParamProperties(param_to_open) + param_properties['highlighted_property'] = highlighted_property # TODO: Which property the cursor will start on + self.param_editor = ParameterEditDialog(self, param_properties, param_to_open) + self.param_editor.returnEditedParamSignal.connect(self.updateParam) + self.param_editor.show() + + def getParamProperties(self, param) -> dict: + """ + Return a dictionary of property name: value pairs for the given parameter + :param param: the parameter to get properties for (QTreeWidgetItem) + """ + properties = {} + properties['name'] = param.text(0) + for property in range(param.childCount()): # Iterate over all properties (children) of the parameter and add them to dict + if param.child(property).text(0) == 'description': + # Access the description text, which is in another sub-item + prop_item = param.child(property).child(0) + properties['description'] = prop_item.text(1) + else: + prop_item = param.child(property) + properties[prop_item.text(0)] = prop_item.text(1) + return properties + + def updateParam(self, updated_param: Parameter, qtree_item: QtWidgets.QTreeWidgetItem): + """ + Update given parameter in the newParamTree with the updated properties + :param updated_param: Sasview Parameter class with updated properties + :param qtree_item: the qtree_item to update + """ + unpacked_param = updated_param[0] # updated_param is sent as a list but will only have one item. Unpack it. + + # Delete all sub-items of the parameter + while qtree_item.childCount() > 0: + sub_item = qtree_item.child(0) + qtree_item.removeChild(sub_item) + + # Now add all the updated properties + self.addSubItems(unpacked_param, qtree_item) + # Make sure to update the name of the parameter + qtree_item.setText(0, unpacked_param.name) + self.badPropsCheck(qtree_item) # Check for bad parameter properties + self.checkDuplicates(self.newParamTree) # Check for duplicate parameter names + + def onApply(self): + """ + Generate output reparameterized model and write to file + """ + # Get the name of the new model + model_name = self.txtNewModelName.text() + overwrite_plugin = self.chkOverwrite.isChecked() + user_plugin_dir = pathlib.Path(find_plugins_dir()) + output_file_path = user_plugin_dir / (model_name + ".py") + + # Check if the file already exists + if os.path.exists(output_file_path) and not overwrite_plugin: + msg = "File with specified name already exists.\n" + msg += "Please specify different filename or allow file overwrite." + QtWidgets.QMessageBox.critical(self, "Overwrite Error", msg) + return + + # Check if the model name is empty + if not model_name: + msg = "No model name specified.\n" + msg += "Please specify a name before continuing." + QtWidgets.QMessageBox.critical(self, "Model Error", msg) + return + + # Check if there are model warnings + param_warnings = False + for i in range(self.newParamTree.topLevelItemCount()): + param = self.newParamTree.topLevelItem(i) + if param.toolTip(1) != "": + param_warnings = True + break + if param_warnings: + # Display a warning allowing the user to cancel or continue + msgBox = QtWidgets.QMessageBox(self) + msgBox.setIcon(QtWidgets.QMessageBox.Warning) + msgBox.setWindowTitle("Model Warning") + msgBox.setText("Some of your parameters contain warnings.\nThis could cause errors or unexpected behavior in the model.") + msgBox.addButton("Continue anyways", QtWidgets.QMessageBox.AcceptRole) + cancelButton = msgBox.addButton(QtWidgets.QMessageBox.Cancel) + + msgBox.exec_() + + # Check which button was clicked + if msgBox.clickedButton() == cancelButton: + # Cancel button clicked + return + + # Write the new model to the file + model_text = self.generateModelText() + self.writeModel(output_file_path, model_text) + self.parent.communicate.customModelDirectoryChanged.emit() # Refresh the list of custom models + + # Test the model for errors (file must be generated first) + error_line = self.checkModel(output_file_path) + if error_line > 0: + return + + self.txtFunction.setStyleSheet("") + self.addTooltips() # Reset the tooltips + + # Notify user that model was written sucessfully + msg = "Reparameterized model "+ model_name + " successfully created." + self.parent.communicate.statusBarUpdateSignal.emit(msg) + logger.info(msg) + + if self.is_modified: + self.is_modified = False + self.setWindowEdited(False) + self.cmdApply.setEnabled(False) + + def generateModelText(self) -> str: + """ + Generate the output model text + """ + output = "" # TODO: Define the output model text, this is just a placeholder function for now + translation_text = self.txtFunction.toPlainText() + old_model_name = self.old_model_name + parameters_text = "" + output_properties = [ + "name", + "units", + "default", + "min", + "max", + "type", + "description" + ] # Order in this list MATTERS! Do not change it; it will change the order of parameter properties in the output model + + # Format the parameters into text for the output file + for i in range(self.newParamTree.topLevelItemCount()): + param = self.newParamTree.topLevelItem(i) + param_properties = self.getParamProperties(param) + parameters_text += "\n\t[ " + for output_property in output_properties: + if output_property not in ('min', 'max'): + # Add the property to the output text + parameters_text += f"{param_properties[output_property]}, " if output_property == "default" else f"'{param_properties[output_property]}', " + # 'min' and 'max' must be formatted together as a list in the output, so we need to handle them separately + elif output_property == 'min': + parameters_text += f"[{param_properties[output_property]}, " + elif output_property == 'max': + parameters_text += f"{param_properties[output_property]}], " + + parameters_text = parameters_text[:-2] # Remove trailing comma and space + parameters_text += "]," + output = REPARAMETARIZED_MODEL_TEMPLATE.format(parameters=parameters_text, translation=translation_text, old_model_name=old_model_name) + return output + + def addSubItems(self, param, top_item): + """ + Add sub-items to the given top-level item for the given parameter + :param param: the Sasmodels Parameter class that contains properties to add + :param top_item: the top-level item to add the properties to (QTreeWidgetItem) + :param append: Whether or not to include parameter when exporting to model file + """ + # Create list of properties: (display name, property name) + properties_index = [ ("default", "default"), + ("min", "limits[0]"), + ("max", "limits[1]"), + ("units", "units"), + ("type", "type") + ] + output_properties = {} # Dictionary of properties used in generating the output model text + for prop in properties_index: + sub_item = QtWidgets.QTreeWidgetItem(top_item) + sub_item.setText(0, prop[0]) # First column is display name + if '[' in prop[1]: + # Limit properties have an index, so we need to extract it + prop_name, index = prop[1].split('[') + index = int(index[:-1]) # Remove the closing ']' and convert to int + # Use getattr to get the property, then index into it + value = getattr(param, prop_name)[index] + sub_item.setText(1, str(value)) + else: + value = getattr(param, prop[1]) + sub_item.setText(1, str(value)) + output_properties[prop[0]] = value # Add property to output dictionary + + # Now add the description as a collapsed item, separate from the other properties + sub_item = QtWidgets.QTreeWidgetItem(top_item) + sub_item.setText(0, "description") + sub_sub_item = QtWidgets.QTreeWidgetItem(sub_item) + description = str(param.description) + sub_sub_item.setText(1, description) + output_properties['name'] = param.name # Add name to output dictionary + output_properties['description'] = description # Add description to output dictionary + + def setWindowEdited(self, is_edited): + """ + Change the widget name to indicate unsaved state + Unsaved state: add "*" to filename display + saved state: remove "*" from filename display + """ + current_text = self.windowTitle() + + if is_edited: + if current_text[-1] != "*": + current_text += "*" + else: + if current_text[-1] == "*": + current_text = current_text[:-1] + self.setWindowTitle(current_text) + + def editorModelModified(self): + """ + User modified the model in the Model Editor. + Disable the plugin editor and show that the model is changed. + """ + #Check to see if model was edited back into original state + f_box = True if self.txtFunction.toPlainText() == "" else False + n_box = True if self.txtNewModelName.text() == "" else False + p_boxes = True if not self.newParamTree.isEnabled() and not self.oldParamTree.isEnabled() else False + + if all([f_box, n_box, p_boxes]): + # Model was edited back into default state, so no need to prompt user to save before exiting + self.setWindowEdited(False) + self.cmdApply.setEnabled(False) + self.is_modified = False + else: + # Otherwise, model was edited and needs to be saved before exiting + self.setWindowEdited(True) + self.cmdApply.setEnabled(True) + self.is_modified = True + + def checkModel(self, full_path): + """ + Run ast and model checks + Attempt to return the line number of the error if any + :param full_path: full path to the model file + :param translation_text: the text within + """ + # successfulCheck = True + error_line = 0 + try: + with open(full_path, 'r', encoding="utf-8") as plugin: + model_str = plugin.read() + ast.parse(model_str) + GuiUtils.checkModel(full_path) + + except Exception as ex: + msg = "Error building model: " + str(ex) + logging.error(msg) + # print four last lines of the stack trace + # this will point out the exact line failing + all_lines = traceback.format_exc().split('\n') + last_lines = all_lines[-4:] + traceback_to_show = '\n'.join(last_lines) + logging.error(traceback_to_show) + + # Set the status bar message + # GuiUtils.Communicate.statusBarUpdateSignal.emit("Model check failed") + self.parent.communicate.statusBarUpdateSignal.emit("Model check failed") + + # Format text box with error indicators + self.txtFunction.setStyleSheet("border: 5px solid red") + # last_lines = traceback.format_exc().split('\n')[-4:] + traceback_to_show = '\n'.join(last_lines) + self.txtFunction.setToolTip(traceback_to_show) + + # attempt to find the failing command line number, usually the last line with + # `File ... line` syntax + reversed_error_text = list(reversed(all_lines)) + for line in reversed_error_text: + if ('File' in line and 'line' in line): + # If model check fails (not syntax) then 'line' and 'File' will be in adjacent lines + error_line = re.split('line ', line)[1] + try: + error_line = int(error_line) + break + except ValueError: + # Sometimes the line number is followed by more text + try: + error_line = error_line.split(',')[0] + error_line = int(error_line) + break + except ValueError: + error_line = 0 + + return error_line + + def badPropsCheck(self, param_item): + """ + Check a parameter for bad properties. + :param param_item: the parameter to check (QTreeWidgetItem) + """ + self.removeParameterWarning(param_item) + + # Get dictionary form of properties for easy manipulation + error_message = "" + properties = {} + properties['name'] = param_item.text(0) + for i in range(param_item.childCount()): + prop = param_item.child(i) + if prop.text(0) == "default": + properties['default'] = prop.text(1) + elif prop.text(0) == "min": + properties['min'] = prop.text(1) + elif prop.text(0) == "max": + properties['max'] = prop.text(1) + elif prop.text(0) == "units": + properties['units'] = prop.text(1) + elif prop.text(0) == "type": + properties['type'] = prop.text(1) + + # Ensure the name is not empty + if properties['name'] == "": + error_message += "Parameter name cannot be empty\n" + + # List of properties to convert to floats + conversion_list = ['min', 'max', 'default'] + + # Ensure that there is at least min, max, and default properties + for item in conversion_list: + if item not in properties or properties[item] == "": + error_message += f"Missing '{item}' property\n" + conversion_list.remove(item) # Remove the property from the list of properties to convert + + # Try to convert min, max, and default to floats + macro_set = {'M_PI', 'M_PI_2', 'M_PI_4', 'M_SQRT1_2', 'M_E', 'M_PI_180', 'M_4PI_3', inf, -inf} + for to_convert in conversion_list: + try: + properties[to_convert] = float(properties[to_convert]) + except ValueError: + if properties[to_convert] in macro_set: + continue # Skip if the value is a macro + else: + error_message += f"'{to_convert}' contains invalid value\n" + + # Check if min is less than max + if 'min' not in error_message and 'max' not in error_message: + if properties['min'] >= properties['max']: + error_message += "Minimum value must be less than maximum value\n" + + # Ensure that units and type are not numbers + for item in ['units', 'type']: + if item in properties: + try: + float(properties[item]) + error_message += f"'{item}' must be a string or left blank.\n" + except ValueError: + pass + + error_message = error_message.strip() # Remove trailing newline + if error_message != "": + # If there are any errors, display a warning icon + self.parameterWarning(param_item, error_message) + + def checkDuplicates(self, tree: QtWidgets.QTreeWidget): + """ + Check for parameters with the same name and display caution icons if found. + NOTE: This method MUST come after badPropsCheck, as badPropsCheck overrides existing tooltip text, + while this method will add to it if needed. + :param tree: the QTreeWidget to check for duplicates. + """ + # If more than one parameter in the tree is the same, display warning icon + count_dict = {} + for i in range(tree.topLevelItemCount()): + item_name = tree.topLevelItem(i).text(0) + if item_name in count_dict: + count_dict[item_name] += 1 + else: + count_dict[item_name] = 1 + + duplicates = [key for key, value in count_dict.items() if value > 1] + + for i in range(tree.topLevelItemCount()): + item = tree.topLevelItem(i) + current_tooltip = item.toolTip(1) + duplicate_warning = "Cannot use duplicate parameter names" + if current_tooltip == duplicate_warning: + # If tooltip is already displaying the warning, clear the warning and then check if it needs to be re-added + # Therefore, we can avoid adding the same warning multiple times + self.removeParameterWarning(item) + + if item.text(0) in duplicates: + if current_tooltip == "": + # If tooltip is empty, show only this warning + updated_tooltip = duplicate_warning + else: + updated_tooltip = current_tooltip + "\n" + duplicate_warning + self.parameterWarning(item, updated_tooltip) + + def parameterWarning(self, table_item, tool_tip_text): + """ + Display a warning icon on a parameter and set tooltip. + :param table_item: The QTreeWidgetItem to add the icon to + :param tool_tip_text: The tooltip text to display when the user hovers over the warning + """ + icon = self.style().standardIcon(QtWidgets.QStyle.SP_MessageBoxWarning) + table_item.setToolTip(1, tool_tip_text) + table_item.setIcon(1, icon) + + def onHelp(self): + """ + Show the "Reparameterization" section of help + """ + tree_location = "/user/qtgui/Perspectives/Fitting/plugin.html#reparameterized-models" + self.parent.showHelp(tree_location) + + def onModelHelp(self): + """ + Show the help page of the loaded model in the OldParamTree + """ + tree_base = "/user/models/" + if self.loaded_model_name is not None: + tree_location = tree_base + f"{self.loaded_model_name}.html" + else: + logging.info("No model detected to have been loaded. Showing default help page.") + tree_location = "/user/qtgui/Perspectives/Fitting/plugin.html#reparameterized-models" + self.parent.showHelp(tree_location) + + ### CLASS METHODS ### + + @classmethod + def removeParameterWarning(cls, table_item): + """ + Remove the warning icon from a parameter + :param table_item: The QTreeWidgetItem to remove the icon from + """ + table_item.setToolTip(1, "") + table_item.setIcon(1, QtGui.QIcon()) + + @classmethod + def getParameterSelection(cls, selected_item) -> QtWidgets.QTreeWidgetItem: + """ + Return the QTreeWidgetItem of the parameter even if selected_item is a 'property' (sub) item + :param selected_item: QTreeWidgetItem that represents either a parameter or a property + """ + if selected_item.parent() == None: + # User selected a parametery, not a property + param_to_open = selected_item + elif selected_item.parent().parent() != None: + # User selected the description text + param_to_open = selected_item.parent().parent() + else: + # User selected a property, not a parameter + param_to_open = selected_item.parent() + return param_to_open + + ### STATIC METHODS ### + + @staticmethod + def writeModel(output_file_path, model_text): + """ + Write the new model to the given file + :param output_file_path: pathlib.Path object pointing to output file location + :param model_text: str of the model text to write + """ + try: + with open(output_file_path, 'w') as f: + f.write(model_text) + except Exception as ex: + logger.error("Error writing model to file: %s" % ex) + + ### OVERRIDES ### + # Functions that overwrite the default behavior of the parent class + + def closeEvent(self, event): + + if self.is_modified: + # Display a warning allowing the user to cancel or continue + msgBox = QtWidgets.QMessageBox(self) + msgBox.setIcon(QtWidgets.QMessageBox.Warning) + msgBox.setWindowTitle("Unsaved Changes") + msgBox.setText("You have unsaved changes. Are you sure you want to close?") + msgBox.addButton("Close without saving", QtWidgets.QMessageBox.AcceptRole) + cancelButton = msgBox.addButton(QtWidgets.QMessageBox.Cancel) + + msgBox.exec_() + if msgBox.clickedButton() == cancelButton: + # Cancel button clicked + event.ignore() + return + + self.close() + self.deleteLater() # Schedule the window for deletion + + +REPARAMETARIZED_MODEL_TEMPLATE = '''\ +from numpy import inf + +from sasmodels.core import reparameterize +from sasmodels.special import * + +parameters = [ + # name, units, default, [min, max], type, description{parameters} +] + +translation = """ + {translation} +""" + +model_info = reparameterize('{old_model_name}', parameters, translation, __file__) + +''' \ No newline at end of file diff --git a/src/sas/qtgui/Utilities/ModelEditors/ReparamEditor/UI/ReparameterizationEditorUI.ui b/src/sas/qtgui/Utilities/ModelEditors/ReparamEditor/UI/ReparameterizationEditorUI.ui new file mode 100644 index 0000000000..e383f38d02 --- /dev/null +++ b/src/sas/qtgui/Utilities/ModelEditors/ReparamEditor/UI/ReparameterizationEditorUI.ui @@ -0,0 +1,369 @@ + + + ReparameterizationEditor + + + Qt::NonModal + + + + 0 + 0 + 723 + 580 + + + + + 0 + 0 + + + + Reparameterization Editor + + + + + + + false + + + + Parameter Redefinition + + + + + + false + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> +p, li { white-space: pre-wrap; } +hr { height: 1px; border-width: 0; } +li.unchecked::marker { content: "\2610"; } +li.checked::marker { content: "\2612"; } +</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Consolas'; font-size:11pt;"><br /></p></body></html> + + + + + + + + + + New Parameters + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 95 + 0 + + + + + 16777215 + 16777215 + + + + Edit Selected + + + + + + + + 0 + 0 + + + + + 25 + 25 + + + + + 20 + true + false + true + + + + + + + - + + + + + + + + 0 + 0 + + + + + 25 + 25 + + + + + 20 + true + + + + + + + + + + + + + + true + + + true + + + true + + + 2 + + + + + + + + + + + + + 0 + 0 + + + + + 50 + 0 + + + + Help + + + false + + + false + + + false + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + Cancel + + + + + + + Apply + + + + + + + + + Model Definition + + + + + + Enter plugin name + + + + + + + Model Name + + + + + + + + + Overwrite existing plugin model + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Select model to be reparameterized + + + + + + + + 0 + 0 + + + + + 100 + 0 + + + + Select Model + + + false + + + false + + + false + + + + + + + + + + + + Old Parameters + + + + + + true + + + false + + + true + + + true + + + 2 + + + + + + + Model Help + + + + + + + + + + + QParameterTreeWidget + QWidget +
sas.qtgui.Utilities.CustomGUI.ParameterTree.h
+
+
+ + +
diff --git a/src/sas/qtgui/Utilities/ModelEditor.py b/src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/ModelEditor.py similarity index 95% rename from src/sas/qtgui/Utilities/ModelEditor.py rename to src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/ModelEditor.py index 0f2c102c7b..255c602630 100644 --- a/src/sas/qtgui/Utilities/ModelEditor.py +++ b/src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/ModelEditor.py @@ -1,7 +1,7 @@ from PySide6 import QtCore from PySide6 import QtWidgets -from sas.qtgui.Utilities.UI.ModelEditor import Ui_ModelEditor +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.UI.ModelEditor import Ui_ModelEditor from sas.qtgui.Utilities import GuiUtils class ModelEditor(QtWidgets.QDialog, Ui_ModelEditor): diff --git a/src/sas/qtgui/Utilities/PluginDefinition.py b/src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/PluginDefinition.py similarity index 98% rename from src/sas/qtgui/Utilities/PluginDefinition.py rename to src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/PluginDefinition.py index fb52ff3432..6974e58f1f 100644 --- a/src/sas/qtgui/Utilities/PluginDefinition.py +++ b/src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/PluginDefinition.py @@ -2,7 +2,7 @@ from PySide6 import QtGui from PySide6 import QtWidgets -from sas.qtgui.Utilities.UI.PluginDefinitionUI import Ui_PluginDefinition +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.UI.PluginDefinitionUI import Ui_PluginDefinition from sas.qtgui.Utilities import GuiUtils # txtName diff --git a/src/sas/qtgui/Utilities/TabbedModelEditor.py b/src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/TabbedModelEditor.py similarity index 98% rename from src/sas/qtgui/Utilities/TabbedModelEditor.py rename to src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/TabbedModelEditor.py index 88328a8c2f..60c35b972c 100644 --- a/src/sas/qtgui/Utilities/TabbedModelEditor.py +++ b/src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/TabbedModelEditor.py @@ -14,9 +14,9 @@ from sas.sascalc.fit.models import find_plugins_dir import sas.qtgui.Utilities.GuiUtils as GuiUtils -from sas.qtgui.Utilities.UI.TabbedModelEditor import Ui_TabbedModelEditor -from sas.qtgui.Utilities.PluginDefinition import PluginDefinition -from sas.qtgui.Utilities.ModelEditor import ModelEditor +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.UI.TabbedModelEditor import Ui_TabbedModelEditor +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.PluginDefinition import PluginDefinition +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.ModelEditor import ModelEditor class TabbedModelEditor(QtWidgets.QDialog, Ui_TabbedModelEditor): """ diff --git a/src/sas/qtgui/Utilities/UI/ModelEditor.ui b/src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/UI/ModelEditor.ui old mode 100755 new mode 100644 similarity index 89% rename from src/sas/qtgui/Utilities/UI/ModelEditor.ui rename to src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/UI/ModelEditor.ui index 4570aa287c..8b07e35177 --- a/src/sas/qtgui/Utilities/UI/ModelEditor.ui +++ b/src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/UI/ModelEditor.ui @@ -1,40 +1,40 @@ - - - ModelEditor - - - - 0 - 0 - 549 - 632 - - - - Model Editor - - - - - - Model - - - - - - - - - - - - - QCodeEditor - QWidget -
sas.qtgui.Utilities.CodeEditor.h
-
-
- - -
+ + + ModelEditor + + + + 0 + 0 + 549 + 632 + + + + Model Editor + + + + + + Model + + + + + + + + + + + + + QCodeEditor + QWidget +
sas.qtgui.Utilities.CustomGUI.CodeEditor.h
+
+
+ + +
diff --git a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui b/src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/UI/PluginDefinitionUI.ui old mode 100755 new mode 100644 similarity index 96% rename from src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui rename to src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/UI/PluginDefinitionUI.ui index 00b365b52b..3e08e2a509 --- a/src/sas/qtgui/Utilities/UI/PluginDefinitionUI.ui +++ b/src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/UI/PluginDefinitionUI.ui @@ -1,158 +1,158 @@ - - - PluginDefinition - - - - 0 - 0 - 723 - 784 - - - - Plugin Definition - - - - - - Plugin name - - - - - - Enter a plugin name - - - - - - - Overwrite existing plugin model of this name - - - - - - - - - - Description - - - - - - Enter a description of the model - - - - - - - - - - Fit parameters - - - - - - Non-polydisperse - - - - - - - - - - - - Parameters - - - - - Initial -value - - - - - - - - - - - Polydisperse - - - - - - - - - - - - Parameters - - - - - Initial -value - - - - - - - - - - - - - - Function(x) - - - - - - false - - - <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> -<html><head><meta name="qrichtext" content="1" /><style type="text/css"> -p, li { white-space: pre-wrap; } -</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:6.6pt; font-weight:400; font-style:normal;"> -<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:7.8pt;"><br /></p></body></html> - - - - - - - - - - txtName - chkOverwrite - txtDescription - tblParams - tblParamsPD - txtFunction - - - - + + + PluginDefinition + + + + 0 + 0 + 723 + 784 + + + + Plugin Definition + + + + + + Plugin name + + + + + + Enter a plugin name + + + + + + + Overwrite existing plugin model of this name + + + + + + + + + + Description + + + + + + Enter a description of the model + + + + + + + + + + Fit parameters + + + + + + Non-polydisperse + + + + + + + + + + + + Parameters + + + + + Initial +value + + + + + + + + + + + Polydisperse + + + + + + + + + + + + Parameters + + + + + Initial +value + + + + + + + + + + + + + + Function(x) + + + + + + false + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><style type="text/css"> +p, li { white-space: pre-wrap; } +</style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:6.6pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-size:7.8pt;"><br /></p></body></html> + + + + + + + + + + txtName + chkOverwrite + txtDescription + tblParams + tblParamsPD + txtFunction + + + + diff --git a/src/sas/qtgui/Utilities/UI/TabbedModelEditor.ui b/src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/UI/TabbedModelEditor.ui old mode 100755 new mode 100644 similarity index 96% rename from src/sas/qtgui/Utilities/UI/TabbedModelEditor.ui rename to src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/UI/TabbedModelEditor.ui index 3c50a083d3..9ef6e27601 --- a/src/sas/qtgui/Utilities/UI/TabbedModelEditor.ui +++ b/src/sas/qtgui/Utilities/ModelEditors/TabbedEditor/UI/TabbedModelEditor.ui @@ -1,66 +1,66 @@ - - - TabbedModelEditor - - - - 0 - 0 - 688 - 697 - - - - Model Editor - - - - :/res/ball.ico:/res/ball.ico - - - - - - -1 - - - - - - - - - Load plugin... - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Qt::Horizontal - - - QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Help - - - - - - - - - - + + + TabbedModelEditor + + + + 0 + 0 + 688 + 697 + + + + Model Editor + + + + :/res/ball.ico:/res/ball.ico + + + + + + -1 + + + + + + + + + Load plugin... + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Help + + + + + + + + + + diff --git a/src/sas/qtgui/Utilities/PluginManager.py b/src/sas/qtgui/Utilities/PluginManager.py index c1880d37dd..2e56bf76c4 100644 --- a/src/sas/qtgui/Utilities/PluginManager.py +++ b/src/sas/qtgui/Utilities/PluginManager.py @@ -6,7 +6,7 @@ from PySide6 import QtWidgets, QtCore from sas.sascalc.fit import models -from sas.qtgui.Utilities.TabbedModelEditor import TabbedModelEditor +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.TabbedModelEditor import TabbedModelEditor import sas.qtgui.Utilities.GuiUtils as GuiUtils from sas.qtgui.Utilities.UI.PluginManagerUI import Ui_PluginManagerUI diff --git a/src/sas/qtgui/Utilities/UnitTesting/AddMultEditorTest.py b/src/sas/qtgui/Utilities/UnitTesting/AddMultEditorTest.py index 9f5d89921b..fc72b2fe98 100644 --- a/src/sas/qtgui/Utilities/UnitTesting/AddMultEditorTest.py +++ b/src/sas/qtgui/Utilities/UnitTesting/AddMultEditorTest.py @@ -12,7 +12,7 @@ from sas.qtgui.Utilities.GuiUtils import Communicate # Local -from sas.qtgui.Utilities.AddMultEditor import AddMultEditor +from sas.qtgui.Utilities.ModelEditors.AddMultEditor.AddMultEditor import AddMultEditor diff --git a/src/sas/qtgui/Utilities/UnitTesting/ModelEditorTest.py b/src/sas/qtgui/Utilities/UnitTesting/ModelEditorTest.py index 062aa8ee29..0dc3ecb035 100755 --- a/src/sas/qtgui/Utilities/UnitTesting/ModelEditorTest.py +++ b/src/sas/qtgui/Utilities/UnitTesting/ModelEditorTest.py @@ -9,7 +9,7 @@ from sas.qtgui.UnitTesting.TestUtils import QtSignalSpy # Local -from sas.qtgui.Utilities.ModelEditor import ModelEditor +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.ModelEditor import ModelEditor from sas.qtgui.Utilities.PythonSyntax import PythonHighlighter diff --git a/src/sas/qtgui/Utilities/UnitTesting/PluginDefinitionTest.py b/src/sas/qtgui/Utilities/UnitTesting/PluginDefinitionTest.py index 0f3ec7a226..b615f20f1a 100644 --- a/src/sas/qtgui/Utilities/UnitTesting/PluginDefinitionTest.py +++ b/src/sas/qtgui/Utilities/UnitTesting/PluginDefinitionTest.py @@ -9,7 +9,7 @@ from sas.qtgui.UnitTesting.TestUtils import QtSignalSpy # Local -from sas.qtgui.Utilities.PluginDefinition import PluginDefinition +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.PluginDefinition import PluginDefinition from sas.qtgui.Utilities.PythonSyntax import PythonHighlighter diff --git a/src/sas/qtgui/Utilities/UnitTesting/TabbedModelEditorTest.py b/src/sas/qtgui/Utilities/UnitTesting/TabbedModelEditorTest.py index 5c2bee2deb..0b8bde7b68 100644 --- a/src/sas/qtgui/Utilities/UnitTesting/TabbedModelEditorTest.py +++ b/src/sas/qtgui/Utilities/UnitTesting/TabbedModelEditorTest.py @@ -12,9 +12,9 @@ # Local import sas.qtgui.Utilities.GuiUtils as GuiUtils -from sas.qtgui.Utilities.TabbedModelEditor import TabbedModelEditor -from sas.qtgui.Utilities.PluginDefinition import PluginDefinition -from sas.qtgui.Utilities.ModelEditor import ModelEditor +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.TabbedModelEditor import TabbedModelEditor +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.PluginDefinition import PluginDefinition +from sas.qtgui.Utilities.ModelEditors.TabbedEditor.ModelEditor import ModelEditor