source: sasview/src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py @ eb1a386

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since eb1a386 was eb1a386, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Fixed text add functionality on plots - SASVIEW-859

  • Property mode set to 100644
File size: 19.6 KB
Line 
1import os
2import sys
3
4import sas.qtgui.Utilities.GuiUtils as GuiUtils
5from PyQt5 import QtGui, QtCore, QtWidgets
6
7import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
8
9from sas.qtgui.Perspectives.Fitting.UI.ConstraintWidgetUI import Ui_ConstraintWidgetUI
10from sas.qtgui.Perspectives.Fitting.FittingWidget import FittingWidget
11from sas.qtgui.Perspectives.Fitting.ComplexConstraint import ComplexConstraint
12from sas.qtgui.Perspectives.Fitting.Constraints import Constraint
13
14class ConstraintWidget(QtWidgets.QWidget, Ui_ConstraintWidgetUI):
15    """
16    Constraints Dialog to select the desired parameter/model constraints.
17    """
18
19    def __init__(self, parent=None):
20        super(ConstraintWidget, self).__init__()
21        self.parent = parent
22        self.setupUi(self)
23        self.currentType = "FitPage"
24
25        # Remember previous content of modified cell
26        self.current_cell = ""
27
28        # Tabs used in simultaneous fitting
29        # tab_name : True/False
30        self.tabs_for_fitting = {}
31
32        # Set up the widgets
33        self.initializeWidgets()
34
35        # Set up signals/slots
36        self.initializeSignals()
37
38        # Create the list of tabs
39        self.initializeFitList()
40
41    def acceptsData(self):
42        """ Tells the caller this widget doesn't accept data """
43        return False
44
45    def initializeWidgets(self):
46        """
47        Set up various widget states
48        """
49        labels = ['FitPage', 'Model', 'Data', 'Mnemonic']
50        # tab widget - headers
51        self.editable_tab_columns = [labels.index('Mnemonic')]
52        self.tblTabList.setColumnCount(len(labels))
53        self.tblTabList.setHorizontalHeaderLabels(labels)
54        self.tblTabList.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
55
56        self.tblTabList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
57        self.tblTabList.customContextMenuRequested.connect(self.showModelContextMenu)
58
59        # disabled constraint
60        labels = ['Constraint']
61        self.tblConstraints.setColumnCount(len(labels))
62        self.tblConstraints.setHorizontalHeaderLabels(labels)
63        self.tblConstraints.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
64        self.tblConstraints.setEnabled(False)
65
66        self.tblConstraints.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
67        self.tblConstraints.customContextMenuRequested.connect(self.showConstrContextMenu)
68
69    def initializeSignals(self):
70        """
71        Set up signals/slots for this widget
72        """
73        # simple widgets
74        self.btnSingle.toggled.connect(self.onFitTypeChange)
75        self.btnBatch.toggled.connect(self.onFitTypeChange)
76        self.cbCases.currentIndexChanged.connect(self.onSpecialCaseChange)
77        self.cmdFit.clicked.connect(self.onFit)
78        self.cmdHelp.clicked.connect(self.onHelp)
79
80        # QTableWidgets
81        self.tblTabList.cellChanged.connect(self.onTabCellEdit)
82        self.tblTabList.cellDoubleClicked.connect(self.onTabCellEntered)
83        self.tblConstraints.cellChanged.connect(self.onConstraintChange)
84
85        # External signals
86        #self.parent.tabsModifiedSignal.connect(self.initializeFitList)
87        self.parent.tabsModifiedSignal.connect(self.onModifiedTabs)
88
89    def updateSignalsFromTab(self, tab=None):
90        """
91        Intercept update signals from fitting tabs
92        """
93        if tab is not None:
94            ObjectLibrary.getObject(tab).constraintAddedSignal.connect(self.initializeFitList)
95            ObjectLibrary.getObject(tab).newModelSignal.connect(self.initializeFitList)
96
97    def onFitTypeChange(self, checked):
98        """
99        Respond to the fit type change
100        single fit/batch fit
101        """
102        source = self.sender().objectName()
103        self.currentType = "BatchPage" if source == "btnBatch" else "FitPage"
104        self.initializeFitList()
105
106    def onSpecialCaseChange(self, index):
107        """
108        Respond to the combobox change for special case constraint sets
109        """
110        pass
111
112    def onFit(self):
113        """
114        Perform the constrained/simultaneous fit
115        """
116        pass
117
118    def onHelp(self):
119        """
120        Display the help page
121        """
122        pass
123
124    def onTabCellEdit(self, row, column):
125        """
126        Respond to check/uncheck and to modify the model moniker actions
127        """
128        item = self.tblTabList.item(row, column)
129        if column == 0:
130            # Update the tabs for fitting list
131            tab_name = item.text()
132            self.tabs_for_fitting[tab_name] = (item.checkState() == QtCore.Qt.Checked)
133            # Enable fitting only when there are models to fit
134            self.cmdFit.setEnabled(any(self.tabs_for_fitting.values()))
135
136        if column not in self.editable_tab_columns:
137            return
138        new_moniker = item.data(0)
139
140        # The new name should be validated on the fly, with QValidator
141        # but let's just assure it post-factum
142        is_good_moniker = self.validateMoniker(new_moniker)
143        if not is_good_moniker:
144            self.tblTabList.blockSignals(True)
145            item.setBackground(QtCore.Qt.red)
146            self.tblTabList.blockSignals(False)
147            self.cmdFit.setEnabled(False)
148            return
149        self.tblTabList.blockSignals(True)
150        item.setBackground(QtCore.Qt.white)
151        self.tblTabList.blockSignals(False)
152        self.cmdFit.setEnabled(True)
153        if not self.current_cell:
154            return
155        # Remember the value
156        if self.current_cell not in self.available_tabs:
157            return
158        temp_tab = self.available_tabs[self.current_cell]
159        # Remove the key from the dictionaries
160        self.available_tabs.pop(self.current_cell, None)
161        # Change the model name
162        model = temp_tab.kernel_module
163        model.name = new_moniker
164        # Replace constraint name
165        temp_tab.replaceConstraintName(self.current_cell, new_moniker)
166        # Replace constraint name in the remaining tabs
167        for tab in self.available_tabs.values():
168            tab.replaceConstraintName(self.current_cell, new_moniker)
169        # Reinitialize the display
170        self.initializeFitList()
171
172    def onConstraintChange(self, row, column):
173        """
174        Modify the constraint in-place.
175        """
176        item = self.tblConstraints.item(row, column)
177        if column == 0:
178            # Update the tabs for fitting list
179            constraint = self.available_constraints[row]
180            constraint.active = (item.checkState() == QtCore.Qt.Checked)
181
182    def onTabCellEntered(self, row, column):
183        """
184        Remember the original tab list cell data.
185        Needed for reverting back on bad validation
186        """
187        if column != 3:
188            return
189        self.current_cell = self.tblTabList.item(row, column).data(0)
190
191    def onModifiedTabs(self):
192        """
193        Respond to tabs being deleted by deleting involved constraints
194
195        This should probably be done in FittingWidget as it is the owner of
196        all the fitting data, but I want to keep the FW oblivious about
197        dependence on other FW tabs, so enforcing the constraint deletion here.
198        """
199        # Get the list of all constraints from querying the table
200        #constraints = getConstraintsForModel()
201
202        # Get the current list of tabs
203        #tabs = ObjectLibrary.listObjects()
204
205        # Check if any of the constraint dependencies got deleted
206        # Check the list of constraints
207        self.initializeFitList()
208        pass
209
210    def isTabImportable(self, tab):
211        """
212        Determines if the tab can be imported and included in the widget
213        """
214        if not self.currentType in tab: return False
215        object = ObjectLibrary.getObject(tab)
216        if not isinstance(object, FittingWidget): return False
217        if object.data is None: return False
218        return True
219
220    def showModelContextMenu(self, position):
221        """
222        Show context specific menu in the tab table widget.
223        """
224        menu = QtWidgets.QMenu()
225        rows = [s.row() for s in self.tblTabList.selectionModel().selectedRows()]
226        num_rows = len(rows)
227        if num_rows <= 0:
228            return
229        # Select for fitting
230        param_string = "Fit Page " if num_rows==1 else "Fit Pages "
231        to_string = "to its current value" if num_rows==1 else "to their current values"
232
233        self.actionSelect = QtWidgets.QAction(self)
234        self.actionSelect.setObjectName("actionSelect")
235        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
236        # Unselect from fitting
237        self.actionDeselect = QtWidgets.QAction(self)
238        self.actionDeselect.setObjectName("actionDeselect")
239        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
240
241        self.actionRemoveConstraint = QtWidgets.QAction(self)
242        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
243        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove all constraints on selected models"))
244
245        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
246        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
247        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of parameters in selected models..."))
248
249        menu.addAction(self.actionSelect)
250        menu.addAction(self.actionDeselect)
251        menu.addSeparator()
252
253        #menu.addAction(self.actionRemoveConstraint)
254        if num_rows >= 2:
255            menu.addAction(self.actionMutualMultiConstrain)
256
257        # Define the callbacks
258        #self.actionConstrain.triggered.connect(self.addSimpleConstraint)
259        #self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
260        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
261        self.actionSelect.triggered.connect(self.selectModels)
262        self.actionDeselect.triggered.connect(self.deselectModels)
263        try:
264            menu.exec_(self.tblTabList.viewport().mapToGlobal(position))
265        except AttributeError as ex:
266            logging.error("Error generating context menu: %s" % ex)
267        return
268
269    def showConstrContextMenu(self, position):
270        """
271        Show context specific menu in the tab table widget.
272        """
273        menu = QtWidgets.QMenu()
274        rows = [s.row() for s in self.tblConstraints.selectionModel().selectedRows()]
275        num_rows = len(rows)
276        if num_rows <= 0:
277            return
278        # Select for fitting
279        param_string = "constraint " if num_rows==1 else "constraints "
280        to_string = "to its current value" if num_rows==1 else "to their current values"
281
282        self.actionSelect = QtWidgets.QAction(self)
283        self.actionSelect.setObjectName("actionSelect")
284        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
285        # Unselect from fitting
286        self.actionDeselect = QtWidgets.QAction(self)
287        self.actionDeselect.setObjectName("actionDeselect")
288        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
289
290        self.actionRemoveConstraint = QtWidgets.QAction(self)
291        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
292        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove "+param_string))
293
294        menu.addAction(self.actionSelect)
295        menu.addAction(self.actionDeselect)
296        menu.addSeparator()
297        menu.addAction(self.actionRemoveConstraint)
298
299        # Define the callbacks
300        #self.actionConstrain.triggered.connect(self.addSimpleConstraint)
301        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
302        #self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
303        self.actionSelect.triggered.connect(self.selectConstraints)
304        self.actionDeselect.triggered.connect(self.deselectConstraints)
305        try:
306            menu.exec_(self.tblConstraints.viewport().mapToGlobal(position))
307        except AttributeError as ex:
308            logging.error("Error generating context menu: %s" % ex)
309        return
310
311    def selectConstraints(self):
312        """
313        Selected constraints are chosen for fitting
314        """
315        status = QtCore.Qt.Checked
316        self.setRowSelection(self.tblConstraints, status)
317
318    def deselectConstraints(self):
319        """
320        Selected constraints are removed for fitting
321        """
322        status = QtCore.Qt.Unchecked
323        self.setRowSelection(self.tblConstraints, status)
324
325    def selectModels(self):
326        """
327        Selected models are chosen for fitting
328        """
329        status = QtCore.Qt.Checked
330        self.setRowSelection(self.tblTabList, status)
331
332    def deselectModels(self):
333        """
334        Selected models are removed for fitting
335        """
336        status = QtCore.Qt.Unchecked
337        self.setRowSelection(self.tblTabList, status)
338
339    def selectedParameters(self, widget):
340        """ Returns list of selected (highlighted) parameters """
341        return [s.row() for s in widget.selectionModel().selectedRows()]
342
343    def setRowSelection(self, widget, status=QtCore.Qt.Unchecked):
344        """
345        Selected models are chosen for fitting
346        """
347        # Convert to proper indices and set requested enablement
348        for row in self.selectedParameters(widget):
349            widget.item(row, 0).setCheckState(status)
350
351    def deleteConstraint(self):#, row):
352        """
353        Delete all selected constraints.
354        """
355        # Removing rows from the table we're iterating over,
356        # so prepare a list of data first
357        constraints_to_delete = []
358        for row in self.selectedParameters(self.tblConstraints):
359            constraints_to_delete.append(self.tblConstraints.item(row, 0).data(0))
360        for constraint in constraints_to_delete:
361            moniker = constraint[:constraint.index(':')]
362            param = constraint[constraint.index(':')+1:constraint.index('=')].strip()
363            tab = self.available_tabs[moniker]
364            tab.deleteConstraintOnParameter(param)
365        # Constraints removed - refresh the table widget
366        self.initializeFitList()
367
368    def uneditableItem(self, data=""):
369        """
370        Returns an uneditable Table Widget Item
371        """
372        item = QtWidgets.QTableWidgetItem(data)
373        item.setFlags( QtCore.Qt.ItemIsSelectable |  QtCore.Qt.ItemIsEnabled )
374        return item
375
376    def updateFitLine(self, tab):
377        """
378        Update a single line of the table widget with tab info
379        """
380        fit_page = ObjectLibrary.getObject(tab)
381        model = fit_page.kernel_module
382        if model is None:
383            return
384        tab_name = tab
385        model_name = model.id
386        moniker = model.name
387        model_data = fit_page.data
388        model_filename = model_data.filename
389        self.available_tabs[moniker] = fit_page
390
391        # Update the model table widget
392        pos = self.tblTabList.rowCount()
393        self.tblTabList.insertRow(pos)
394        item = self.uneditableItem(tab_name)
395        item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
396        if tab_name in self.tabs_for_fitting:
397            state = QtCore.Qt.Checked if self.tabs_for_fitting[tab_name] else QtCore.Qt.Unchecked
398            item.setCheckState(state)
399        else:
400            item.setCheckState(QtCore.Qt.Checked)
401            self.tabs_for_fitting[tab_name] = True
402
403        self.tblTabList.setItem(pos, 0, item)
404        self.tblTabList.setItem(pos, 1, self.uneditableItem(model_name))
405        self.tblTabList.setItem(pos, 2, self.uneditableItem(model_filename))
406        # Moniker is editable, so no option change
407        item = QtWidgets.QTableWidgetItem(moniker)
408        # Disable signals so we don't get infinite call recursion
409        self.tblTabList.blockSignals(True)
410        self.tblTabList.setItem(pos, 3, item)
411        self.tblTabList.blockSignals(False)
412
413        # Check if any constraints present in tab
414        constraint_names = fit_page.getConstraintsForModel()
415        constraints = fit_page.getConstraintObjectsForModel()
416        if not constraints: 
417            return
418        self.tblConstraints.setEnabled(True)
419        for constraint, constraint_name in zip(constraints, constraint_names):
420            # Create the text for widget item
421            label = moniker + ":"+ constraint_name[0] + " = " + constraint_name[1]
422            pos = self.tblConstraints.rowCount()
423            self.available_constraints[pos] = constraint
424
425            # Show the text in the constraint table
426            item = self.uneditableItem(label)
427            item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
428            item.setCheckState(QtCore.Qt.Checked)
429            self.tblConstraints.insertRow(pos)
430            self.tblConstraints.setItem(pos, 0, item)
431
432    def initializeFitList(self):
433        """
434        Fill the list of model/data sets for fitting/constraining
435        """
436        # look at the object library to find all fit tabs
437        # Show the content of the current "model"
438        objects = ObjectLibrary.listObjects()
439
440        # Tab dict
441        # moniker -> (kernel_module, data)
442        self.available_tabs = {}
443        # Constraint dict
444        # moniker -> [constraints]
445        self.available_constraints = {}
446
447        # Reset the table widgets
448        self.tblTabList.setRowCount(0)
449        self.tblConstraints.setRowCount(0)
450
451        # Fit disabled
452        self.cmdFit.setEnabled(False)
453
454        if not objects:
455            return
456
457        tabs = [tab for tab in ObjectLibrary.listObjects() if self.isTabImportable(tab)]
458        for tab in tabs:
459            self.updateFitLine(tab)
460            self.updateSignalsFromTab(tab)
461            # We have at least 1 fit page, allow fitting
462            self.cmdFit.setEnabled(True)
463
464    def validateMoniker(self, new_moniker=None):
465        """
466        Check new_moniker for correctness.
467        It must be non-empty.
468        It must not be the same as other monikers.
469        """
470        if not new_moniker:
471            return False
472
473        for existing_moniker in self.available_tabs:
474            if existing_moniker == new_moniker and existing_moniker != self.current_cell:
475                return False
476
477        return True
478
479    def getObjectByName(self, name):
480        for object_name in ObjectLibrary.listObjects():
481            object = ObjectLibrary.getObject(object_name)
482            if isinstance(object, FittingWidget):
483                try:
484                    if object.kernel_module.name == name:
485                        return object
486                except AttributeError:
487                    # Disregard atribute errors - empty fit widgets
488                    continue
489        return None
490
491    def showMultiConstraint(self):
492        """
493        Invoke the complex constraint editor
494        """
495        selected_rows = self.selectedParameters(self.tblTabList)
496        assert(len(selected_rows)==2)
497
498        tab_list = [ObjectLibrary.getObject(self.tblTabList.item(s, 0).data(0)) for s in selected_rows]
499        # Create and display the widget for param1 and param2
500        cc_widget = ComplexConstraint(self, tabs=tab_list)
501        if cc_widget.exec_() != QtWidgets.QDialog.Accepted:
502            return
503
504        constraint = Constraint()
505        model1, param1, operator, constraint_text = cc_widget.constraint()
506
507        constraint.func = constraint_text
508        constraint.param = param1
509        # Find the right tab
510        constrained_tab = self.getObjectByName(model1)
511        if constrained_tab is None:
512            return
513
514        # Find the constrained parameter row
515        constrained_row = constrained_tab.getRowFromName(param1)
516
517        # Update the tab
518        constrained_tab.addConstraintToRow(constraint, constrained_row)
519        pass
Note: See TracBrowser for help on using the repository browser.