source: sasview/src/sas/qtgui/Utilities/AddMultEditor.py @ 6c7ebb88

Last change on this file since 6c7ebb88 was 33c0561, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

Replace Apply button menu driven functionality with additional button.
Removed Cancel.
Removed the window system context help button from all affected widgets.
SASVIEW-1239

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