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

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 14ec91c5 was 14ec91c5, checked in by Piotr Rozyczko <rozyczko@…>, 4 years ago

More code review related fixes

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