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

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

Disconnect slots before reconnecting them again.

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