Index: src/sas/qtgui/Perspectives/Fitting/ComplexConstraint.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/ComplexConstraint.py	(revision 2d466e4b832e803d9f9cf7561ec93be0e476045a)
+++ src/sas/qtgui/Perspectives/Fitting/ComplexConstraint.py	(revision 2d466e4b832e803d9f9cf7561ec93be0e476045a)
@@ -0,0 +1,200 @@
+"""
+Widget for multi-model constraints.
+"""
+import os
+from numpy import *
+
+from PyQt5 import QtCore
+from PyQt5 import QtGui
+from PyQt5 import QtWidgets
+import webbrowser
+
+import sas.qtgui.Utilities.GuiUtils as GuiUtils
+ALLOWED_OPERATORS = ['=','<','>','>=','<=']
+
+# Local UI
+from sas.qtgui.Perspectives.Fitting.UI.ComplexConstraintUI import Ui_ComplexConstraintUI
+
+class ComplexConstraint(QtWidgets.QDialog, Ui_ComplexConstraintUI):
+    def __init__(self, parent=None, tabs=None):
+        super(ComplexConstraint, self).__init__()
+
+        self.setupUi(self)
+        #self.setFixedSize(self.minimumSizeHint())
+        self.setModal(True)
+
+        # Useful globals
+        self.tabs = tabs
+        self.params = None
+        self.tab_names = None
+        self.operator = '='
+
+        self.setupData()
+        self.setupWidgets()
+        self.setupSignals()
+        #self.setupTooltip()
+
+        self.setFixedSize(self.minimumSizeHint())
+
+    def setupData(self):
+        """
+        Digs into self.tabs and pulls out relevant info
+        """
+        self.tab_names = [tab.kernel_module.name for tab in self.tabs]
+        self.params = [tab.getParamNames() for tab in self.tabs]
+        pass
+
+    def setupSignals(self):
+        """
+        Signals from various elements
+        """
+        self.cmdOK.clicked.connect(self.accept)
+        self.cmdHelp.clicked.connect(self.onHelp)
+        self.cmdRevert.clicked.connect(self.onRevert)
+        #self.txtConstraint.editingFinished.connect(self.validateFormula)
+
+        self.cbParam1.currentIndexChanged.connect(self.onParamIndexChange)
+        self.cbParam2.currentIndexChanged.connect(self.onParamIndexChange)
+        self.cbOperator.currentIndexChanged.connect(self.onOperatorChange)
+
+    def setupWidgets(self):
+        """
+        Setup widgets based on current parameters
+        """
+        self.txtName1.setText(self.tab_names[0])
+        self.txtName2.setText(self.tab_names[1])
+
+        self.cbParam1.clear()
+        self.cbParam1.addItems(self.params[0])
+        self.cbParam2.clear()
+        self.cbParam2.addItems(self.params[1])
+
+        self.txtParam.setText(self.tab_names[0] + ":" + self.cbParam1.currentText())
+
+        self.cbOperator.clear()
+        self.cbOperator.addItems(ALLOWED_OPERATORS)
+        self.txtOperator.setText(self.cbOperator.currentText())
+
+        self.txtConstraint.setText(self.tab_names[1]+"."+self.cbParam2.currentText())
+
+    def setupTooltip(self):
+        """
+        Tooltip for txtConstraint
+        """
+        tooltip = "E.g.\n%s = 2.0 * (%s)\n" %(self.params[0], self.params[1])
+        tooltip += "%s = sqrt(%s) + 5"%(self.params[0], self.params[1])
+        self.txtConstraint.setToolTip(tooltip)
+
+    def onParamIndexChange(self, index):
+        """
+        Respond to parameter combo box changes
+        """
+        # Find out the signal source
+        source = self.sender().objectName()
+        if source == "cbParam1":
+            self.txtParam.setText(self.tab_names[0] + ":" + self.cbParam1.currentText())
+        else:
+            self.txtConstraint.setText(self.cbParam2.currentText())
+        pass
+
+    def onOperatorChange(self, index):
+        """
+        Respond to operator combo box changes
+        """
+        self.txtOperator.setText(self.cbOperator.currentText())
+
+    def onRevert(self):
+        """
+        switch M1 <-> M2
+        """
+        # Switch parameters
+        self.params[1], self.params[0] = self.params[0], self.params[1]
+        self.tab_names[1], self.tab_names[0] = self.tab_names[0], self.tab_names[1]
+        # Try to swap parameter names in the line edit
+        current_text = self.txtConstraint.text()
+        new_text = current_text.replace(self.cbParam1.currentText(), self.cbParam2.currentText())
+        self.txtConstraint.setText(new_text)
+        # Update labels and tooltips
+        index1 = self.cbParam1.currentIndex()
+        index2 = self.cbParam2.currentIndex()
+        indexOp = self.cbOperator.currentIndex()
+        self.setupWidgets()
+
+        # Original indices
+        self.cbParam1.setCurrentIndex(index2)
+        self.cbParam2.setCurrentIndex(index1)
+        self.cbOperator.setCurrentIndex(indexOp)
+        #self.setupTooltip()
+
+    def validateFormula(self):
+        """
+        Add visual cues when formula is incorrect
+        """
+        formula_is_valid = False
+        formula_is_valid = self.validateConstraint(self.txtConstraint.text())
+        if not formula_is_valid:
+            self.cmdOK.setEnabled(False)
+            self.txtConstraint.setStyleSheet("QLineEdit {background-color: red;}")
+        else:
+            self.cmdOK.setEnabled(True)
+            self.txtConstraint.setStyleSheet("QLineEdit {background-color: white;}")
+
+    def validateConstraint(self, constraint_text):
+        """
+        Ensure the constraint has proper form
+        """
+        # 0. none or empty
+        if not constraint_text or not isinstance(constraint_text, str):
+            return False
+
+        param_str = str(self.params[1])
+        constraint_text = constraint_text.strip()
+
+        # 1. just the parameter
+        if param_str == constraint_text:
+            return True
+
+        # 2. ensure the text contains parameter name
+        parameter_string_start = constraint_text.find(param_str)
+        if parameter_string_start < 0:
+            return False
+        parameter_string_end = parameter_string_start + len(param_str)
+
+        # 3. parameter name should be a separate word, but can have "()[]*+-/ " around
+        valid_neighbours = "()[]*+-/ "
+        has_only_parameter = False
+        start_loc = parameter_string_start -1
+        end_loc = parameter_string_end
+        if not any([constraint_text[start_loc] == char for char in valid_neighbours]):
+            return False
+        if end_loc < len(constraint_text):
+            if not any([constraint_text[end_loc] == char for char in valid_neighbours]):
+                return False
+
+        # 4. replace parameter name with "1" and try to evaluate the expression
+        try:
+            expression_to_evaluate = constraint_text.replace(param_str, "1.0")
+            eval(expression_to_evaluate)
+        except Exception:
+            # Too many cases to cover individually, just a blanket
+            # Exception should be sufficient
+            # Note that in current numpy things like sqrt(-1) don't
+            # raise but just return warnings
+            return False
+
+        return True
+
+    def onHelp(self):
+        """
+        Display related help section
+        """
+        try:
+            help_location = GuiUtils.HELP_DIRECTORY_LOCATION + \
+            "/user/sasgui/perspectives/fitting/fitting_help.html#simultaneous-fits-with-constraints"
+            webbrowser.open('file://' + os.path.realpath(help_location))
+        except AttributeError:
+            # No manager defined - testing and standalone runs
+            pass
+
+
+
Index: src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py	(revision 47d7d2d2d39efdc6f1e4deaf9560dee9d004810a)
+++ src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py	(revision 2d466e4b832e803d9f9cf7561ec93be0e476045a)
@@ -9,4 +9,5 @@
 from sas.qtgui.Perspectives.Fitting.UI.ConstraintWidgetUI import Ui_ConstraintWidgetUI
 from sas.qtgui.Perspectives.Fitting.FittingWidget import FittingWidget
+from sas.qtgui.Perspectives.Fitting.ComplexConstraint import ComplexConstraint
 
 class ConstraintWidget(QtWidgets.QWidget, Ui_ConstraintWidgetUI):
@@ -434,3 +435,14 @@
         Invoke the complex constraint editor
         """
-        pass
+        selected_rows = self.selectedParameters(self.tblTabList)
+        assert(len(selected_rows)==2)
+
+        tab_list = [ObjectLibrary.getObject(self.tblTabList.item(s, 0).data(0)) for s in selected_rows]
+        # Create and display the widget for param1 and param2
+        cc_widget = ComplexConstraint(self, tabs=tab_list)
+        if cc_widget.exec_() != QtWidgets.QDialog.Accepted:
+            return
+
+        #constraint = Constraint()
+        #c_text = cc_widget.txtConstraint.text()
+        pass
Index: src/sas/qtgui/Perspectives/Fitting/FittingWidget.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision 47d7d2d2d39efdc6f1e4deaf9560dee9d004810a)
+++ src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision 2d466e4b832e803d9f9cf7561ec93be0e476045a)
@@ -567,6 +567,4 @@
         constraint.param = mc_widget.params[0]
         # Function should have the model name preamble
-        #model_name = self.kernel_module.name
-        #constraint.func = model_name + ":" + c_text
         constraint.func = c_text
 
@@ -576,5 +574,5 @@
 
         # Which row is the constrained parameter in?
-        row = self.rowFromName(constraint.param)
+        row = self.getRowFromName(constraint.param)
         self._model_model.item(row, 1).setChild(0, item)
         self.constraintAddedSignal.emit([row])
@@ -589,5 +587,5 @@
         self.communicate.statusBarUpdateSignal.emit('Constraints added')
 
-    def rowFromName(self, name):
+    def getRowFromName(self, name):
         """
         Given parameter name get the row number in self._model_model
@@ -598,4 +596,14 @@
                 return row
         return None
+
+    def getParamNames(self):
+        """
+        Return list of all parameters for the current model
+        """
+        #params = []
+        #for row in range(self._model_model.rowCount()):
+        #    params.append(self._model_model.item(row).text())
+        #return params
+        return [self._model_model.item(row).text() for row in range(self._model_model.rowCount())]
 
     def modifyViewOnRow(self, row, font=None, brush=None):
Index: src/sas/qtgui/Perspectives/Fitting/UI/ComplexConstraintUI.ui
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/UI/ComplexConstraintUI.ui	(revision 2d466e4b832e803d9f9cf7561ec93be0e476045a)
+++ src/sas/qtgui/Perspectives/Fitting/UI/ComplexConstraintUI.ui	(revision 2d466e4b832e803d9f9cf7561ec93be0e476045a)
@@ -0,0 +1,222 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>ComplexConstraintUI</class>
+ <widget class="QDialog" name="ComplexConstraintUI">
+  <property name="geometry">
+   <rect>
+    <x>0</x>
+    <y>0</y>
+    <width>359</width>
+    <height>166</height>
+   </rect>
+  </property>
+  <property name="windowTitle">
+   <string>Complex Constraint</string>
+  </property>
+  <layout class="QGridLayout" name="gridLayout_2">
+   <item row="0" column="0">
+    <widget class="QGroupBox" name="groupBox">
+     <property name="title">
+      <string>2 parameter constraint</string>
+     </property>
+     <layout class="QGridLayout" name="gridLayout">
+      <item row="0" column="0">
+       <layout class="QHBoxLayout" name="horizontalLayout">
+        <item>
+         <widget class="QLabel" name="txtName1">
+          <property name="text">
+           <string>name1</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="cbParam1">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <item>
+           <property name="text">
+            <string>sld</string>
+           </property>
+          </item>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="cbOperator">
+          <item>
+           <property name="text">
+            <string>&gt;</string>
+           </property>
+          </item>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLabel" name="txtName2">
+          <property name="text">
+           <string>name2</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QComboBox" name="cbParam2">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <item>
+           <property name="text">
+            <string>sld_a</string>
+           </property>
+          </item>
+         </widget>
+        </item>
+        <item>
+         <spacer name="horizontalSpacer">
+          <property name="orientation">
+           <enum>Qt::Horizontal</enum>
+          </property>
+          <property name="sizeHint" stdset="0">
+           <size>
+            <width>40</width>
+            <height>20</height>
+           </size>
+          </property>
+         </spacer>
+        </item>
+        <item>
+         <widget class="QPushButton" name="cmdRevert">
+          <property name="text">
+           <string>Revert</string>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+      <item row="1" column="0">
+       <spacer name="verticalSpacer">
+        <property name="orientation">
+         <enum>Qt::Vertical</enum>
+        </property>
+        <property name="sizeHint" stdset="0">
+         <size>
+          <width>20</width>
+          <height>25</height>
+         </size>
+        </property>
+       </spacer>
+      </item>
+      <item row="2" column="0">
+       <layout class="QHBoxLayout" name="horizontalLayout_2">
+        <item>
+         <widget class="QLabel" name="txtParam">
+          <property name="sizePolicy">
+           <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+            <horstretch>0</horstretch>
+            <verstretch>0</verstretch>
+           </sizepolicy>
+          </property>
+          <property name="text">
+           <string>param1</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLabel" name="txtOperator">
+          <property name="text">
+           <string>=</string>
+          </property>
+         </widget>
+        </item>
+        <item>
+         <widget class="QLineEdit" name="txtConstraint">
+          <property name="text">
+           <string/>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </item>
+     </layout>
+    </widget>
+   </item>
+   <item row="1" column="0">
+    <layout class="QHBoxLayout" name="horizontalLayout_3">
+     <item>
+      <spacer name="horizontalSpacer_2">
+       <property name="orientation">
+        <enum>Qt::Horizontal</enum>
+       </property>
+       <property name="sizeHint" stdset="0">
+        <size>
+         <width>40</width>
+         <height>20</height>
+        </size>
+       </property>
+      </spacer>
+     </item>
+     <item>
+      <widget class="QPushButton" name="cmdOK">
+       <property name="text">
+        <string>OK</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="cmdCancel">
+       <property name="text">
+        <string>Cancel</string>
+       </property>
+      </widget>
+     </item>
+     <item>
+      <widget class="QPushButton" name="cmdHelp">
+       <property name="text">
+        <string>Help</string>
+       </property>
+      </widget>
+     </item>
+    </layout>
+   </item>
+  </layout>
+ </widget>
+ <resources/>
+ <connections>
+  <connection>
+   <sender>cmdOK</sender>
+   <signal>clicked()</signal>
+   <receiver>ComplexConstraintUI</receiver>
+   <slot>accept()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>149</x>
+     <y>144</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>179</x>
+     <y>82</y>
+    </hint>
+   </hints>
+  </connection>
+  <connection>
+   <sender>cmdCancel</sender>
+   <signal>clicked()</signal>
+   <receiver>ComplexConstraintUI</receiver>
+   <slot>reject()</slot>
+   <hints>
+    <hint type="sourcelabel">
+     <x>230</x>
+     <y>144</y>
+    </hint>
+    <hint type="destinationlabel">
+     <x>179</x>
+     <y>82</y>
+    </hint>
+   </hints>
+  </connection>
+ </connections>
+</ui>
