source: sasview/src/sas/qtgui/Utilities/AddMultEditor.py @ 60a4e71

Last change on this file since 60a4e71 was 287d356, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Don't wantonly cast parameter names into lower case. SASVIEW-1016

  • Property mode set to 100644
File size: 11.0 KB
Line 
1"""
2Widget for simple add / multiply editor.
3"""
4# numpy methods required for the validator! Don't remove.
5# pylint: disable=unused-import,unused-wildcard-import,redefined-builtin
6from numpy import *
7
8from PyQt5 import QtCore
9from PyQt5 import QtGui
10from PyQt5 import QtWidgets
11import webbrowser
12
13import os
14import numpy as np
15import re
16import logging
17import traceback
18
19import sas.qtgui.Utilities.GuiUtils as GuiUtils
20from sasmodels.sasview_model import load_standard_models
21from sas.qtgui.Perspectives.Fitting import ModelUtilities
22
23# Local UI
24from sas.qtgui.Utilities.UI.AddMultEditorUI import Ui_AddMultEditorUI
25
26# Template for the output plugin file
27SUM_TEMPLATE = """
28from sasmodels.core import load_model_info
29from sasmodels.sasview_model import make_model_from_info
30
31model_info = load_model_info('{model1}{operator}{model2}')
32model_info.name = '{name}'
33model_info.description = '{desc_line}'
34Model = make_model_from_info(model_info)
35"""
36
37# Color of backgrounds to underline valid or invalid input
38BG_WHITE = "background-color: rgb(255, 255, 255);"
39BG_RED = "background-color: rgb(244, 170, 164);"
40
41
42class AddMultEditor(QtWidgets.QDialog, Ui_AddMultEditorUI):
43    """
44       Dialog for easy custom composite models.  Provides a Dialog panel
45       to choose two existing models (including pre-existing Plugin Models which
46       may themselves be composite models) as well as an operation on those models
47       (add or multiply) the resulting model will add a scale parameter and a
48       background parameter.
49       The user can also give a brief help for the model in the description box and
50       must provide a unique name which is verified before the new model is saved.
51    """
52    def __init__(self, parent=None):
53        super(AddMultEditor, self).__init__(parent._parent)
54
55        self.parent = parent
56
57        self.setupUi(self)
58
59        #  uncheck self.chkOverwrite
60        self.chkOverwrite.setChecked(False)
61        self.canOverwriteName = False
62
63        # Disabled Apply button until input of valid output plugin name
64        self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
65
66        # Flag for correctness of resulting name
67        self.good_name = False
68
69        self.setupSignals()
70
71        self.list_models = self.readModels()
72
73        # Fill models' comboboxes
74        self.setupModels()
75
76        self.setFixedSize(self.minimumSizeHint())
77
78        # Name and directory for saving new plugin model
79        self.plugin_filename = None
80        self.plugin_dir = ModelUtilities.find_plugins_dir()
81
82        # Validators
83        rx = QtCore.QRegExp("^[A-Za-z0-9_]*$")
84        txt_validator = QtGui.QRegExpValidator(rx)
85        self.txtName.setValidator(txt_validator)
86
87    def setupModels(self):
88        """ Add list of models to 'Model1' and 'Model2' comboboxes """
89        # Load the model dict
90        self.cbModel1.addItems(self.list_models)
91        self.cbModel2.addItems(self.list_models)
92
93        # set the default initial value of Model1 and Model2
94        index_ini_model1 = self.cbModel1.findText('sphere', QtCore.Qt.MatchFixedString)
95
96        if index_ini_model1 >= 0:
97            self.cbModel1.setCurrentIndex(index_ini_model1)
98        else:
99            self.cbModel1.setCurrentIndex(0)
100
101        index_ini_model2 = self.cbModel2.findText('cylinder',
102                                                  QtCore.Qt.MatchFixedString)
103        if index_ini_model2 >= 0:
104            self.cbModel2.setCurrentIndex(index_ini_model2)
105        else:
106            self.cbModel2.setCurrentIndex(0)
107
108    def readModels(self):
109        """ Generate list of models """
110        models = load_standard_models()
111        models_dict = {}
112        for model in models:
113            models_dict[model.name] = model
114
115        return sorted([model_name for model_name in models_dict])
116
117    def setupSignals(self):
118        """ Signals from various elements """
119        # check existence of output filename when entering name
120        # or when overwriting not allowed
121        self.txtName.editingFinished.connect(self.onNameCheck)
122        self.chkOverwrite.stateChanged.connect(self.onOverwrite)
123
124        self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).clicked.connect(self.onApply)
125        self.buttonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self.onHelp)
126        self.buttonBox.button(QtWidgets.QDialogButtonBox.Close).clicked.connect(self.close)
127
128        # change displayed equation when changing operator
129        self.cbOperator.currentIndexChanged.connect(self.onOperatorChange)
130
131    def onOverwrite(self):
132        """
133        Modify state on checkbox change
134        """
135        self.canOverwriteName = self.chkOverwrite.isChecked()
136        # State changed -> allow Apply
137        self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(True)
138
139    def onNameCheck(self):
140        """
141        Check if proposed new model name does not already exists
142        (if the overwriting is not allowed).
143        If not an error message not show error message is displayed
144        """
145
146        # Get the function/file name
147        title = self.txtName.text().lstrip().rstrip()
148
149        filename = title + '.py'
150
151        if self.canOverwriteName:
152            # allow overwriting -> only valid name needs to be checked
153            # (done with validator in __init__ above)
154            self.good_name = True
155            self.txtName.setStyleSheet(BG_WHITE)
156            self.plugin_filename = os.path.join(self.plugin_dir, filename)
157        else:
158            # No overwriting -> check existence of filename
159            # Create list of existing model names for comparison
160            # fake existing regular model name list
161            models_list = [item + '.py' for item in self.list_models]
162            if filename in models_list:
163                self.good_name = False
164                self.txtName.setStyleSheet(BG_RED)
165                msg = "Plugin with specified name already exists.\n"
166                msg += "Please specify different filename or allow file overwrite."
167                logging.warning(msg)
168                QtWidgets.QMessageBox.critical(self, 'Plugin Error', msg)
169            else:
170                s_title = title
171                if len(title) > 20:
172                    s_title = title[0:19] + '...'
173                logging.info("Model function ({}) has been set!\n".
174                                format(str(s_title)))
175                self.good_name = True
176                self.txtName.setStyleSheet(BG_WHITE)
177                self.plugin_filename = os.path.join(self.plugin_dir, filename)
178
179        # Enable Apply push button only if valid name
180        self.buttonBox.button(
181            QtWidgets.QDialogButtonBox.Apply).setEnabled(self.good_name)
182
183    def onOperatorChange(self, index):
184        """ Respond to operator combo box changes """
185
186        self.lblEquation.setText("Plugin_model = scale_factor * "
187                                 "(model_1 {} model_2) + background".
188                                 format(self.cbOperator.currentText()))
189
190    def onApply(self):
191        """ Validity check, save model to file """
192
193        # Set the button enablement, so no double clicks can be made
194        self.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).setEnabled(False)
195
196        # Check the name/overwrite combination again, in case we managed to confuse the UI
197        self.onNameCheck()
198        if not self.good_name:
199            return
200
201        self.write_new_model_to_file(self.plugin_filename,
202                                     self.cbModel1.currentText(),
203                                     self.cbModel2.currentText(),
204                                     self.cbOperator.currentText())
205
206        try:
207            success = GuiUtils.checkModel(self.plugin_filename)
208        except Exception as ex:
209            # broad exception from sasmodels
210            msg = "Error building model: "+ str(ex)
211            logging.error(msg)
212            #print three last lines of the stack trace
213            # this will point out the exact line failing
214            last_lines = traceback.format_exc().split('\n')[-4:]
215            traceback_to_show = '\n'.join(last_lines)
216            logging.error(traceback_to_show)
217
218            # Set the status bar message
219            self.parent.communicate.statusBarUpdateSignal.emit("Model check failed")
220            return
221
222        if not success:
223            return
224
225        # Update list of models in FittingWidget and AddMultEditor
226        self.parent.communicate.customModelDirectoryChanged.emit()
227        # Re-read the model list so the new model is included
228        self.list_models = self.readModels()
229        self.updateModels()
230
231        # Notify the user
232        title = self.txtName.text().lstrip().rstrip()
233        msg = "Custom model "+title + " successfully created."
234        self.parent.communicate.statusBarUpdateSignal.emit(msg)
235        logging.info(msg)
236
237
238    def write_new_model_to_file(self, fname, model1_name, model2_name, operator):
239        """ Write and Save file """
240
241        description = self.txtDescription.text().lstrip().rstrip()
242        if description.strip() != '':
243            # Sasmodels generates a description for us. If the user provides
244            # their own description, add a line to overwrite the sasmodels one
245            desc_line = description
246        else:
247            desc_line = "{} {} {}".format(model1_name,
248                                          operator,
249                                          model2_name)
250
251        name = os.path.splitext(os.path.basename(fname))[0]
252        output = SUM_TEMPLATE.format(name=name,
253                                     model1=model1_name,
254                                     model2=model2_name,
255                                     operator=operator,
256                                     desc_line=desc_line)
257
258        with open(fname, 'w') as out_f:
259            out_f.write(output)
260
261    def updateModels(self):
262        """ Update contents of comboboxes with new plugin models """
263
264        # Keep pointers to the current indices so we can show the comboboxes with
265        # original selection
266        model_1 = self.cbModel1.currentText()
267        model_2 = self.cbModel2.currentText()
268
269        self.cbModel1.blockSignals(True)
270        self.cbModel1.clear()
271        self.cbModel1.blockSignals(False)
272
273        self.cbModel2.blockSignals(True)
274        self.cbModel2.clear()
275        self.cbModel2.blockSignals(False)
276        # Retrieve the list of models
277        model_list = self.readModels()
278        # Populate the models comboboxes
279        self.cbModel1.addItems(model_list)
280        self.cbModel2.addItems(model_list)
281
282        # Scroll back to the user chosen models
283        self.cbModel1.setCurrentIndex(self.cbModel1.findText(model_1))
284        self.cbModel2.setCurrentIndex(self.cbModel2.findText(model_2))
285
286    def onHelp(self):
287        """ Display related help section """
288
289        try:
290            help_location = GuiUtils.HELP_DIRECTORY_LOCATION + \
291                            "/user/qtgui/Perspectives/Fitting/fitting_help.html#sum-multi-p1-p2"
292            webbrowser.open('file://' + os.path.realpath(help_location))
293        except AttributeError:
294            # No manager defined - testing and standalone runs
295            pass
Note: See TracBrowser for help on using the repository browser.