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