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

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 17968c3 was 17968c3, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Support for running C&S batch fits - SASVIEW-865

  • Property mode set to 100644
File size: 24.8 KB
Line 
1import os
2import sys
3
4from twisted.internet import threads
5
6import sas.qtgui.Utilities.GuiUtils as GuiUtils
7from PyQt5 import QtGui, QtCore, QtWidgets
8
9from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
10
11import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
12from sas.qtgui.Perspectives.Fitting.UI.ConstraintWidgetUI import Ui_ConstraintWidgetUI
13from sas.qtgui.Perspectives.Fitting.FittingWidget import FittingWidget
14from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
15from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
16from sas.qtgui.Perspectives.Fitting.ComplexConstraint import ComplexConstraint
17from sas.qtgui.Perspectives.Fitting.Constraints import Constraint
18
19class ConstraintWidget(QtWidgets.QWidget, Ui_ConstraintWidgetUI):
20    """
21    Constraints Dialog to select the desired parameter/model constraints.
22    """
23
24    def __init__(self, parent=None, tab_id=1):
25        super(ConstraintWidget, self).__init__()
26        self.parent = parent
27        self.setupUi(self)
28        self.currentType = "FitPage"
29        self.tab_id = tab_id
30        # Page id for fitting
31        # To keep with previous SasView values, use 300 as the start offset
32        self.page_id = 300 + self.tab_id
33
34        # Remember previous content of modified cell
35        self.current_cell = ""
36
37        # Tabs used in simultaneous fitting
38        # tab_name : True/False
39        self.tabs_for_fitting = {}
40
41        # Set up the widgets
42        self.initializeWidgets()
43
44        # Set up signals/slots
45        self.initializeSignals()
46
47        # Create the list of tabs
48        self.initializeFitList()
49
50    def acceptsData(self):
51        """ Tells the caller this widget doesn't accept data """
52        return False
53
54    def initializeWidgets(self):
55        """
56        Set up various widget states
57        """
58        labels = ['FitPage', 'Model', 'Data', 'Mnemonic']
59        # tab widget - headers
60        self.editable_tab_columns = [labels.index('Mnemonic')]
61        self.tblTabList.setColumnCount(len(labels))
62        self.tblTabList.setHorizontalHeaderLabels(labels)
63        self.tblTabList.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
64
65        self.tblTabList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
66        self.tblTabList.customContextMenuRequested.connect(self.showModelContextMenu)
67
68        # disabled constraint
69        labels = ['Constraint']
70        self.tblConstraints.setColumnCount(len(labels))
71        self.tblConstraints.setHorizontalHeaderLabels(labels)
72        self.tblConstraints.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
73        self.tblConstraints.setEnabled(False)
74
75        self.tblConstraints.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
76        self.tblConstraints.customContextMenuRequested.connect(self.showConstrContextMenu)
77
78    def initializeSignals(self):
79        """
80        Set up signals/slots for this widget
81        """
82        # simple widgets
83        self.btnSingle.toggled.connect(self.onFitTypeChange)
84        self.btnBatch.toggled.connect(self.onFitTypeChange)
85        self.cbCases.currentIndexChanged.connect(self.onSpecialCaseChange)
86        self.cmdFit.clicked.connect(self.onFit)
87        self.cmdHelp.clicked.connect(self.onHelp)
88
89        # QTableWidgets
90        self.tblTabList.cellChanged.connect(self.onTabCellEdit)
91        self.tblTabList.cellDoubleClicked.connect(self.onTabCellEntered)
92        self.tblConstraints.cellChanged.connect(self.onConstraintChange)
93
94        # External signals
95        self.parent.tabsModifiedSignal.connect(self.onModifiedTabs)
96
97    def updateSignalsFromTab(self, tab=None):
98        """
99        Intercept update signals from fitting tabs
100        """
101        if tab is not None:
102            ObjectLibrary.getObject(tab).constraintAddedSignal.connect(self.initializeFitList)
103            ObjectLibrary.getObject(tab).newModelSignal.connect(self.initializeFitList)
104
105    def onFitTypeChange(self, checked):
106        """
107        Respond to the fit type change
108        single fit/batch fit
109        """
110        source = self.sender().objectName()
111        self.currentType = "BatchPage" if source == "btnBatch" else "FitPage"
112        self.initializeFitList()
113
114    def onSpecialCaseChange(self, index):
115        """
116        Respond to the combobox change for special case constraint sets
117        """
118        pass
119
120    def onFit(self):
121        """
122        Perform the constrained/simultaneous fit
123        """
124        # Find out all tabs to fit
125        tabs_to_fit = [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
126
127        # Single fitter for the simultaneous run
128        sim_fitter = Fit()
129        sim_fitter.fitter_id = self.page_id
130
131        # prepare fitting problems for each tab
132        #
133        page_ids = []
134        fitter_id = 0
135        sim_fitter=[sim_fitter]
136        # Prepare the fitter object
137        try:
138            for tab in tabs_to_fit:
139                tab_object = ObjectLibrary.getObject(tab)
140                if tab_object is None:
141                    # No such tab!
142                    return
143                sim_fitter, fitter_id = tab_object.prepareFitters(fitter=sim_fitter[0], fit_id=fitter_id)
144                page_ids.append([tab_object.page_id])
145        except ValueError as ex:
146            # No parameters selected in one of the tabs
147            no_params_msg = "Fitting can not be performed.\n" +\
148                            "Not all tabs chosen for fitting have parameters selected for fitting."
149            reply = QtWidgets.QMessageBox.question(self,
150                                               'Warning',
151                                               no_params_msg,
152                                               QtWidgets.QMessageBox.Ok)
153
154            return
155
156        # Create the fitting thread, based on the fitter
157        completefn = self.onBatchFitComplete if self.currentType=='BatchPage' else self.onFitComplete
158
159        #if USING_TWISTED:
160        handler = None
161        updater = None
162        #else:
163        #    handler = ConsoleUpdate(parent=self.parent,
164        #                            manager=self,
165        #                            improvement_delta=0.1)
166        #    updater = handler.update_fit
167
168        batch_inputs = {}
169        batch_outputs = {}
170
171        # new fit thread object
172        calc_fit = FitThread(handler=handler,
173                             fn=sim_fitter,
174                             batch_inputs=batch_inputs,
175                             batch_outputs=batch_outputs,
176                             page_id=page_ids,
177                             updatefn=updater,
178                             completefn=completefn)
179
180        #if USING_TWISTED:
181        # start the trhrhread with twisted
182        calc_thread = threads.deferToThread(calc_fit.compute)
183        calc_thread.addCallback(completefn)
184        calc_thread.addErrback(self.onFitFailed)
185        #else:
186        #    # Use the old python threads + Queue
187        #    calc_fit.queue()
188        #    calc_fit.ready(2.5)
189
190
191        #disable the Fit button
192        self.cmdFit.setText('Running...')
193        self.parent.communicate.statusBarUpdateSignal.emit('Fitting started...')
194        self.cmdFit.setEnabled(False)
195
196    def onHelp(self):
197        """
198        Show the "Fitting" section of help
199        """
200        tree_location = "/user/sasgui/perspectives/fitting/"
201
202        helpfile = "fitting_help.html#simultaneous-fit-mode"
203        help_location = tree_location + helpfile
204
205        # OMG, really? Crawling up the object hierarchy...
206        self.parent.parent.showHelp(help_location)
207
208    def onTabCellEdit(self, row, column):
209        """
210        Respond to check/uncheck and to modify the model moniker actions
211        """
212        item = self.tblTabList.item(row, column)
213        if column == 0:
214            # Update the tabs for fitting list
215            tab_name = item.text()
216            self.tabs_for_fitting[tab_name] = (item.checkState() == QtCore.Qt.Checked)
217            # Enable fitting only when there are models to fit
218            self.cmdFit.setEnabled(any(self.tabs_for_fitting.values()))
219
220        if column not in self.editable_tab_columns:
221            return
222        new_moniker = item.data(0)
223
224        # The new name should be validated on the fly, with QValidator
225        # but let's just assure it post-factum
226        is_good_moniker = self.validateMoniker(new_moniker)
227        if not is_good_moniker:
228            self.tblTabList.blockSignals(True)
229            item.setBackground(QtCore.Qt.red)
230            self.tblTabList.blockSignals(False)
231            self.cmdFit.setEnabled(False)
232            return
233        self.tblTabList.blockSignals(True)
234        item.setBackground(QtCore.Qt.white)
235        self.tblTabList.blockSignals(False)
236        self.cmdFit.setEnabled(True)
237        if not self.current_cell:
238            return
239        # Remember the value
240        if self.current_cell not in self.available_tabs:
241            return
242        temp_tab = self.available_tabs[self.current_cell]
243        # Remove the key from the dictionaries
244        self.available_tabs.pop(self.current_cell, None)
245        # Change the model name
246        model = temp_tab.kernel_module
247        model.name = new_moniker
248        # Replace constraint name
249        temp_tab.replaceConstraintName(self.current_cell, new_moniker)
250        # Replace constraint name in the remaining tabs
251        for tab in self.available_tabs.values():
252            tab.replaceConstraintName(self.current_cell, new_moniker)
253        # Reinitialize the display
254        self.initializeFitList()
255
256    def onConstraintChange(self, row, column):
257        """
258        Modify the constraint's "active" instance variable.
259        """
260        item = self.tblConstraints.item(row, column)
261        if column == 0:
262            # Update the tabs for fitting list
263            constraint = self.available_constraints[row]
264            constraint.active = (item.checkState() == QtCore.Qt.Checked)
265
266    def onTabCellEntered(self, row, column):
267        """
268        Remember the original tab list cell data.
269        Needed for reverting back on bad validation
270        """
271        if column != 3:
272            return
273        self.current_cell = self.tblTabList.item(row, column).data(0)
274
275    def onModifiedTabs(self):
276        """
277        Respond to tabs being deleted by deleting involved constraints
278
279        This should probably be done in FittingWidget as it is the owner of
280        all the fitting data, but I want to keep the FW oblivious about
281        dependence on other FW tabs, so enforcing the constraint deletion here.
282        """
283        # Get the list of all constraints from querying the table
284        #constraints = getConstraintsForModel()
285
286        # Get the current list of tabs
287        #tabs = ObjectLibrary.listObjects()
288
289        # Check if any of the constraint dependencies got deleted
290        # Check the list of constraints
291        self.initializeFitList()
292        pass
293
294    def onFitComplete(self, result):
295        """
296        Respond to the successful fit complete signal
297        """
298        #re-enable the Fit button
299        self.cmdFit.setText("Fit")
300        self.cmdFit.setEnabled(True)
301
302        # get the elapsed time
303        elapsed = result[1]
304
305        # result list
306        results = result[0][0]
307
308        # Find out all tabs to fit
309        tabs_to_fit = [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
310
311        # update all involved tabs
312        for i, tab in enumerate(tabs_to_fit):
313            tab_object = ObjectLibrary.getObject(tab)
314            if tab_object is None:
315                # No such tab. removed while job was running
316                return
317            # Make sure result and target objects are the same (same model moniker)
318            if tab_object.kernel_module.name == results[i].model.name:
319                tab_object.fitComplete(([[results[i]]], elapsed))
320
321        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
322        self.parent.communicate.statusBarUpdateSignal.emit(msg)
323
324    def onBatchFitComplete(self, result):
325        """
326        Respond to the successful batch fit complete signal
327        """
328        #re-enable the Fit button
329        self.cmdFit.setText("Fit")
330        self.cmdFit.setEnabled(True)
331
332        # get the elapsed time
333        elapsed = result[1]
334
335        # ADD THE BATCH FIT VIEW HERE
336        #
337
338        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
339        self.parent.communicate.statusBarUpdateSignal.emit(msg)
340
341        pass
342
343    def onFitFailed(self, reason):
344        """
345        Respond to fitting failure.
346        """
347        #re-enable the Fit button
348        self.cmdFit.setText("Fit")
349        self.cmdFit.setEnabled(True)
350
351        msg = "Fitting failed: %s s.\n" % reason
352        self.parent.communicate.statusBarUpdateSignal.emit(msg)
353        pass
354 
355    def isTabImportable(self, tab):
356        """
357        Determines if the tab can be imported and included in the widget
358        """
359        if not self.currentType in tab: return False
360        object = ObjectLibrary.getObject(tab)
361        if not isinstance(object, FittingWidget): return False
362        if object.data is None: return False
363        return True
364
365    def showModelContextMenu(self, position):
366        """
367        Show context specific menu in the tab table widget.
368        """
369        menu = QtWidgets.QMenu()
370        rows = [s.row() for s in self.tblTabList.selectionModel().selectedRows()]
371        num_rows = len(rows)
372        if num_rows <= 0:
373            return
374        # Select for fitting
375        param_string = "Fit Page " if num_rows==1 else "Fit Pages "
376        to_string = "to its current value" if num_rows==1 else "to their current values"
377
378        self.actionSelect = QtWidgets.QAction(self)
379        self.actionSelect.setObjectName("actionSelect")
380        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
381        # Unselect from fitting
382        self.actionDeselect = QtWidgets.QAction(self)
383        self.actionDeselect.setObjectName("actionDeselect")
384        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
385
386        self.actionRemoveConstraint = QtWidgets.QAction(self)
387        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
388        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove all constraints on selected models"))
389
390        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
391        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
392        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of parameters in selected models..."))
393
394        menu.addAction(self.actionSelect)
395        menu.addAction(self.actionDeselect)
396        menu.addSeparator()
397
398        #menu.addAction(self.actionRemoveConstraint)
399        if num_rows >= 2:
400            menu.addAction(self.actionMutualMultiConstrain)
401
402        # Define the callbacks
403        #self.actionConstrain.triggered.connect(self.addSimpleConstraint)
404        #self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
405        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
406        self.actionSelect.triggered.connect(self.selectModels)
407        self.actionDeselect.triggered.connect(self.deselectModels)
408        try:
409            menu.exec_(self.tblTabList.viewport().mapToGlobal(position))
410        except AttributeError as ex:
411            logging.error("Error generating context menu: %s" % ex)
412        return
413
414    def showConstrContextMenu(self, position):
415        """
416        Show context specific menu in the tab table widget.
417        """
418        menu = QtWidgets.QMenu()
419        rows = [s.row() for s in self.tblConstraints.selectionModel().selectedRows()]
420        num_rows = len(rows)
421        if num_rows <= 0:
422            return
423        # Select for fitting
424        param_string = "constraint " if num_rows==1 else "constraints "
425        to_string = "to its current value" if num_rows==1 else "to their current values"
426
427        self.actionSelect = QtWidgets.QAction(self)
428        self.actionSelect.setObjectName("actionSelect")
429        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
430        # Unselect from fitting
431        self.actionDeselect = QtWidgets.QAction(self)
432        self.actionDeselect.setObjectName("actionDeselect")
433        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
434
435        self.actionRemoveConstraint = QtWidgets.QAction(self)
436        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
437        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove "+param_string))
438
439        menu.addAction(self.actionSelect)
440        menu.addAction(self.actionDeselect)
441        menu.addSeparator()
442        menu.addAction(self.actionRemoveConstraint)
443
444        # Define the callbacks
445        #self.actionConstrain.triggered.connect(self.addSimpleConstraint)
446        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
447        #self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
448        self.actionSelect.triggered.connect(self.selectConstraints)
449        self.actionDeselect.triggered.connect(self.deselectConstraints)
450        try:
451            menu.exec_(self.tblConstraints.viewport().mapToGlobal(position))
452        except AttributeError as ex:
453            logging.error("Error generating context menu: %s" % ex)
454        return
455
456    def selectConstraints(self):
457        """
458        Selected constraints are chosen for fitting
459        """
460        status = QtCore.Qt.Checked
461        self.setRowSelection(self.tblConstraints, status)
462
463    def deselectConstraints(self):
464        """
465        Selected constraints are removed for fitting
466        """
467        status = QtCore.Qt.Unchecked
468        self.setRowSelection(self.tblConstraints, status)
469
470    def selectModels(self):
471        """
472        Selected models are chosen for fitting
473        """
474        status = QtCore.Qt.Checked
475        self.setRowSelection(self.tblTabList, status)
476
477    def deselectModels(self):
478        """
479        Selected models are removed for fitting
480        """
481        status = QtCore.Qt.Unchecked
482        self.setRowSelection(self.tblTabList, status)
483
484    def selectedParameters(self, widget):
485        """ Returns list of selected (highlighted) parameters """
486        return [s.row() for s in widget.selectionModel().selectedRows()]
487
488    def setRowSelection(self, widget, status=QtCore.Qt.Unchecked):
489        """
490        Selected models are chosen for fitting
491        """
492        # Convert to proper indices and set requested enablement
493        for row in self.selectedParameters(widget):
494            widget.item(row, 0).setCheckState(status)
495
496    def deleteConstraint(self):#, row):
497        """
498        Delete all selected constraints.
499        """
500        # Removing rows from the table we're iterating over,
501        # so prepare a list of data first
502        constraints_to_delete = []
503        for row in self.selectedParameters(self.tblConstraints):
504            constraints_to_delete.append(self.tblConstraints.item(row, 0).data(0))
505        for constraint in constraints_to_delete:
506            moniker = constraint[:constraint.index(':')]
507            param = constraint[constraint.index(':')+1:constraint.index('=')].strip()
508            tab = self.available_tabs[moniker]
509            tab.deleteConstraintOnParameter(param)
510        # Constraints removed - refresh the table widget
511        self.initializeFitList()
512
513    def uneditableItem(self, data=""):
514        """
515        Returns an uneditable Table Widget Item
516        """
517        item = QtWidgets.QTableWidgetItem(data)
518        item.setFlags( QtCore.Qt.ItemIsSelectable |  QtCore.Qt.ItemIsEnabled )
519        return item
520
521    def updateFitLine(self, tab):
522        """
523        Update a single line of the table widget with tab info
524        """
525        fit_page = ObjectLibrary.getObject(tab)
526        model = fit_page.kernel_module
527        if model is None:
528            return
529        tab_name = tab
530        model_name = model.id
531        moniker = model.name
532        model_data = fit_page.data
533        model_filename = model_data.filename
534        self.available_tabs[moniker] = fit_page
535
536        # Update the model table widget
537        pos = self.tblTabList.rowCount()
538        self.tblTabList.insertRow(pos)
539        item = self.uneditableItem(tab_name)
540        item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
541        if tab_name in self.tabs_for_fitting:
542            state = QtCore.Qt.Checked if self.tabs_for_fitting[tab_name] else QtCore.Qt.Unchecked
543            item.setCheckState(state)
544        else:
545            item.setCheckState(QtCore.Qt.Checked)
546            self.tabs_for_fitting[tab_name] = True
547
548        self.tblTabList.setItem(pos, 0, item)
549        self.tblTabList.setItem(pos, 1, self.uneditableItem(model_name))
550        self.tblTabList.setItem(pos, 2, self.uneditableItem(model_filename))
551        # Moniker is editable, so no option change
552        item = QtWidgets.QTableWidgetItem(moniker)
553        # Disable signals so we don't get infinite call recursion
554        self.tblTabList.blockSignals(True)
555        self.tblTabList.setItem(pos, 3, item)
556        self.tblTabList.blockSignals(False)
557
558        # Check if any constraints present in tab
559        constraint_names = fit_page.getConstraintsForModel()
560        constraints = fit_page.getConstraintObjectsForModel()
561        if not constraints: 
562            return
563        self.tblConstraints.setEnabled(True)
564        for constraint, constraint_name in zip(constraints, constraint_names):
565            # Create the text for widget item
566            label = moniker + ":"+ constraint_name[0] + " = " + constraint_name[1]
567            pos = self.tblConstraints.rowCount()
568            self.available_constraints[pos] = constraint
569
570            # Show the text in the constraint table
571            item = self.uneditableItem(label)
572            item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
573            item.setCheckState(QtCore.Qt.Checked)
574            self.tblConstraints.insertRow(pos)
575            self.tblConstraints.setItem(pos, 0, item)
576
577    def initializeFitList(self):
578        """
579        Fill the list of model/data sets for fitting/constraining
580        """
581        # look at the object library to find all fit tabs
582        # Show the content of the current "model"
583        objects = ObjectLibrary.listObjects()
584
585        # Tab dict
586        # moniker -> (kernel_module, data)
587        self.available_tabs = {}
588        # Constraint dict
589        # moniker -> [constraints]
590        self.available_constraints = {}
591
592        # Reset the table widgets
593        self.tblTabList.setRowCount(0)
594        self.tblConstraints.setRowCount(0)
595
596        # Fit disabled
597        self.cmdFit.setEnabled(False)
598
599        if not objects:
600            return
601
602        tabs = [tab for tab in ObjectLibrary.listObjects() if self.isTabImportable(tab)]
603        for tab in tabs:
604            self.updateFitLine(tab)
605            self.updateSignalsFromTab(tab)
606            # We have at least 1 fit page, allow fitting
607            self.cmdFit.setEnabled(True)
608
609    def validateMoniker(self, new_moniker=None):
610        """
611        Check new_moniker for correctness.
612        It must be non-empty.
613        It must not be the same as other monikers.
614        """
615        if not new_moniker:
616            return False
617
618        for existing_moniker in self.available_tabs:
619            if existing_moniker == new_moniker and existing_moniker != self.current_cell:
620                return False
621
622        return True
623
624    def getObjectByName(self, name):
625        for object_name in ObjectLibrary.listObjects():
626            object = ObjectLibrary.getObject(object_name)
627            if isinstance(object, FittingWidget):
628                try:
629                    if object.kernel_module.name == name:
630                        return object
631                except AttributeError:
632                    # Disregard atribute errors - empty fit widgets
633                    continue
634        return None
635
636    def showMultiConstraint(self):
637        """
638        Invoke the complex constraint editor
639        """
640        selected_rows = self.selectedParameters(self.tblTabList)
641        assert(len(selected_rows)==2)
642
643        tab_list = [ObjectLibrary.getObject(self.tblTabList.item(s, 0).data(0)) for s in selected_rows]
644        # Create and display the widget for param1 and param2
645        cc_widget = ComplexConstraint(self, tabs=tab_list)
646        if cc_widget.exec_() != QtWidgets.QDialog.Accepted:
647            return
648
649        constraint = Constraint()
650        model1, param1, operator, constraint_text = cc_widget.constraint()
651
652        constraint.func = constraint_text
653        constraint.param = param1
654        # Find the right tab
655        constrained_tab = self.getObjectByName(model1)
656        if constrained_tab is None:
657            return
658
659        # Find the constrained parameter row
660        constrained_row = constrained_tab.getRowFromName(param1)
661
662        # Update the tab
663        constrained_tab.addConstraintToRow(constraint, constrained_row)
664        pass
Note: See TracBrowser for help on using the repository browser.