Index: src/sas/qtgui/Perspectives/Fitting/AssociatedComboBox.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/AssociatedComboBox.py	(revision 04f775db13543cbc91b64834af6068a163b77f8b)
+++ src/sas/qtgui/Perspectives/Fitting/AssociatedComboBox.py	(revision 04f775db13543cbc91b64834af6068a163b77f8b)
@@ -0,0 +1,38 @@
+from PyQt5 import QtGui, QtWidgets
+
+
+class AssociatedComboBox(QtWidgets.QComboBox):
+    """Just a regular combo box, but associated with a particular QStandardItem so that it can be used to display and
+    select an item's current text and a restricted list of other possible text.
+
+    When the combo box's current text is changed, the change is immediately reflected in the associated item; either
+    the text itself is set as the item's data, or the current index is used; see constructor."""
+    item = None  # type: QtGui.QStandardItem
+
+    def __init__(self, item, idx_as_value=False, parent=None):
+        # type: (QtGui.QStandardItem, bool, QtWidgets.QWidget) -> None
+        """
+        Initialize widget. idx_as_value indicates whether to use the combo box's current index (otherwise, current text)
+        as the associated item's new data.
+        """
+        super(AssociatedComboBox, self).__init__(parent)
+        self.item = item
+
+        if idx_as_value:
+            self.currentIndexChanged[int].connect(self._onIndexChanged)
+        else:
+            self.currentTextChanged.connect(self._onTextChanged)
+
+    def _onTextChanged(self, text):
+        # type: (str) -> None
+        """
+        Callback for combo box's currentTextChanged. Set associated item's data to the new text.
+        """
+        self.item.setText(text)
+
+    def _onIndexChanged(self, idx):
+        # type: (int) -> None
+        """
+        Callback for combo box's currentIndexChanged. Set associated item's data to the new index.
+        """
+        self.item.setText(str(idx))
Index: src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py	(revision 70f445859abd127c3fa7cd9f6b8e2700e7bcb1a2)
+++ src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py	(revision 70f445859abd127c3fa7cd9f6b8e2700e7bcb1a2)
@@ -8,4 +8,6 @@
 from sas.qtgui.Plotting.PlotterData import Data1D
 from sas.qtgui.Plotting.PlotterData import Data2D
+
+from sas.qtgui.Perspectives.Fitting.AssociatedComboBox import AssociatedComboBox
 
 model_header_captions = ['Parameter', 'Value', 'Min', 'Max', 'Units']
@@ -61,7 +63,37 @@
     return (param_name, param_length)
 
-def addParametersToModel(parameters, kernel_module, is2D):
-    """
-    Update local ModelModel with sasmodel parameters
+def createFixedChoiceComboBox(param, item_row):
+    """
+    Determines whether param is a fixed-choice parameter, modifies items in item_row appropriately and returns a combo
+    box containing the fixed choices. Returns None if param is not fixed-choice.
+    
+    item_row is a list of QStandardItem objects for insertion into the parameter table. 
+    """
+
+    # Determine whether this is a fixed-choice parameter. There are lots of conditionals, simply because the
+    # implementation is not yet concrete; there are several possible indicators that the parameter is fixed-choice.
+    # TODO: (when the sasmodels implementation is concrete, clean this up)
+    choices = None
+    if isinstance(param.choices, (list, tuple)) and len(param.choices) > 0:
+        # The choices property is concrete in sasmodels, probably will use this
+        choices = param.choices
+    elif isinstance(param.units, (list, tuple)):
+        choices = [str(x) for x in param.units]
+
+    cbox = None
+    if choices is not None:
+        # Use combo box for input, if it is fixed-choice
+        cbox = AssociatedComboBox(item_row[1], idx_as_value=True)
+        cbox.addItems(choices)
+        item_row[2].setEditable(False)
+        item_row[3].setEditable(False)
+
+    return cbox
+
+def addParametersToModel(parameters, kernel_module, is2D, model=None, view=None):
+    """
+    Update local ModelModel with sasmodel parameters.
+    Actually appends to model, if model and view params are not None.
+    Always returns list of lists of QStandardItems.
     """
     multishell_parameters = getIterParams(parameters)
@@ -72,19 +104,20 @@
     else:
         params = parameters.iq_parameters
-    item = []
+
+    rows = []
     for param in params:
         # don't include shell parameters
         if param.name == multishell_param_name:
             continue
+
         # Modify parameter name from <param>[n] to <param>1
         item_name = param.name
         if param in multishell_parameters:
             continue
-        #    item_name = replaceShellName(param.name, 1)
 
         item1 = QtGui.QStandardItem(item_name)
         item1.setCheckable(True)
         item1.setEditable(False)
-        # item_err = QtGui.QStandardItem()
+
         # check for polydisp params
         if param.polydisperse:
@@ -93,4 +126,5 @@
             item1_1 = QtGui.QStandardItem("Distribution")
             item1_1.setEditable(False)
+
             # Find param in volume_params
             for p in parameters.form_volume_parameters:
@@ -99,5 +133,4 @@
                 width = kernel_module.getParam(p.name+'.width')
                 ptype = kernel_module.getParam(p.name+'.type')
-
                 item1_2 = QtGui.QStandardItem(str(width))
                 item1_2.setEditable(False)
@@ -110,23 +143,36 @@
                 poly_item.appendRow([item1_1, item1_2, item1_3, item1_4, item1_5])
                 break
+
             # Add the polydisp item as a child
             item1.appendRow([poly_item])
+
         # Param values
         item2 = QtGui.QStandardItem(str(param.default))
-        # TODO: the error column.
-        # Either add a proxy model or a custom view delegate
-        #item_err = QtGui.QStandardItem()
         item3 = QtGui.QStandardItem(str(param.limits[0]))
         item4 = QtGui.QStandardItem(str(param.limits[1]))
-        item5 = QtGui.QStandardItem(param.units)
+        item5 = QtGui.QStandardItem(str(param.units))
         item5.setEditable(False)
-        item.append([item1, item2, item3, item4, item5])
-    return item
-
-def addSimpleParametersToModel(parameters, is2D, parameters_original=None):
-    """
-    Update local ModelModel with sasmodel parameters
-    parameters_original: list of parameters before any tagging on their IDs, e.g. for product model
-    (so that those are the display names; see below)
+
+        # Check if fixed-choice (returns combobox, if so, also makes some items uneditable)
+        row = [item1, item2, item3, item4, item5]
+        cbox = createFixedChoiceComboBox(param, row)
+
+        # Append to the model and use the combobox, if required
+        if None not in (model, view):
+            model.appendRow(row)
+            if cbox:
+                view.setIndexWidget(item2.index(), cbox)
+        rows.append(row)
+
+    return rows
+
+def addSimpleParametersToModel(parameters, is2D, parameters_original=None, model=None, view=None):
+    """
+    Update local ModelModel with sasmodel parameters (non-dispersed, non-magnetic)
+    Actually appends to model, if model and view params are not None.
+    Always returns list of lists of QStandardItems.
+
+    parameters_original: list of parameters before any tagging on their IDs, e.g. for product model (so that those are
+    the display names; see below)
     """
     if is2D:
@@ -147,5 +193,5 @@
         params_orig = params
 
-    item = []
+    rows = []
     for param, param_orig in zip(params, params_orig):
         # Create the top level, checkable item
@@ -155,13 +201,25 @@
         item1.setCheckable(True)
         item1.setEditable(False)
+
         # Param values
         # TODO: add delegate for validation of cells
         item2 = QtGui.QStandardItem(str(param.default))
-        item4 = QtGui.QStandardItem(str(param.limits[0]))
-        item5 = QtGui.QStandardItem(str(param.limits[1]))
-        item6 = QtGui.QStandardItem(str(param.units))
-        item6.setEditable(False)
-        item.append([item1, item2, item4, item5, item6])
-    return item
+        item3 = QtGui.QStandardItem(str(param.limits[0]))
+        item4 = QtGui.QStandardItem(str(param.limits[1]))
+        item5 = QtGui.QStandardItem(str(param.units))
+        item5.setEditable(False)
+
+        # Check if fixed-choice (returns combobox, if so, also makes some items uneditable)
+        row = [item1, item2, item3, item4, item5]
+        cbox = createFixedChoiceComboBox(param, row)
+
+        # Append to the model and use the combobox, if required
+        if None not in (model, view):
+            model.appendRow(row)
+            if cbox:
+                view.setIndexWidget(item2.index(), cbox)
+        rows.append(row)
+
+    return rows
 
 def markParameterDisabled(model, row):
@@ -259,8 +317,9 @@
     model.header_tooltips = copy.copy(poly_header_error_tooltips)
 
-def addShellsToModel(parameters, model, index, row_num=None):
+def addShellsToModel(parameters, model, index, row_num=None, view=None):
     """
     Find out multishell parameters and update the model with the requested number of them.
     Inserts them after the row at row_num, if not None; otherwise, appends to end.
+    If view param is not None, supports fixed-choice params.
     Returns a list of lists of QStandardItem objects.
     """
@@ -285,5 +344,5 @@
                     item1_3 = QtGui.QStandardItem(str(p.limits[0]))
                     item1_4 = QtGui.QStandardItem(str(p.limits[1]))
-                    item1_5 = QtGui.QStandardItem(p.units)
+                    item1_5 = QtGui.QStandardItem(str(p.units))
                     poly_item.appendRow([item1_1, item1_2, item1_3, item1_4, item1_5])
                     break
@@ -293,8 +352,12 @@
             item3 = QtGui.QStandardItem(str(par.limits[0]))
             item4 = QtGui.QStandardItem(str(par.limits[1]))
-            item5 = QtGui.QStandardItem(par.units)
+            item5 = QtGui.QStandardItem(str(par.units))
+            item5.setEditable(False)
+
+            # Check if fixed-choice (returns combobox, if so, also makes some items uneditable)
             row = [item1, item2, item3, item4, item5]
-            rows.append(row)
-
+            cbox = createFixedChoiceComboBox(par, row)
+
+            # Always add to the model
             if row_num is None:
                 model.appendRow(row)
@@ -302,4 +365,10 @@
                 model.insertRow(row_num, row)
                 row_num += 1
+
+            # Apply combobox if required
+            if None not in (view, cbox):
+                view.setIndexWidget(item2.index(), cbox)
+
+            rows.append(row)
 
     return rows
Index: src/sas/qtgui/Perspectives/Fitting/FittingWidget.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision c2f3ca2c72aeba33d19dcf49f5793abf90fa7a3b)
+++ src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision 5fb714bc8d76b96b914937e72cffc3f835224dd1)
@@ -2073,13 +2073,14 @@
         self.shell_names = self.shellNamesList()
 
-        # Get new rows for QModel
-        new_rows = FittingUtilities.addParametersToModel(self.model_parameters, self.kernel_module, self.is2D)
-
         # Add heading row
         FittingUtilities.addHeadingRowToModel(self._model_model, model_name)
 
-        # Update QModel
-        for row in new_rows:
-            self._model_model.appendRow(row)
+        # Update the QModel
+        FittingUtilities.addParametersToModel(
+                self.model_parameters,
+                self.kernel_module,
+                self.is2D,
+                self._model_model,
+                self.lstParams)
 
     def fromStructureFactorToQModel(self, structure_factor):
@@ -2090,13 +2091,14 @@
             return
 
-        s_kernel = self.models[structure_factor]()
-        p_kernel = self.kernel_module
-
-        if p_kernel is None:
-            # Not a product model, just S(Q)
-            self.kernel_module = s_kernel
-            params = modelinfo.ParameterTable(self.kernel_module._model_info.parameters.kernel_parameters)
-            new_rows = FittingUtilities.addSimpleParametersToModel(params, self.is2D)
+        if self.kernel_module is None:
+            # Structure factor is the only selected model; build it and show all its params
+            self.kernel_module = self.models[structure_factor]()
+            s_params = self.kernel_module._model_info.parameters
+            s_params_orig = s_params
+
         else:
+            s_kernel = self.models[structure_factor]()
+            p_kernel = self.kernel_module
+
             p_pars_len = len(p_kernel._model_info.parameters.kernel_parameters)
             s_pars_len = len(s_kernel._model_info.parameters.kernel_parameters)
@@ -2107,17 +2109,6 @@
 
             # S(Q) params from the product model are not necessarily the same as those from the S(Q) model; any
-            # conflicting names with P(Q) params will cause a rename; we also lose radius_effective (for now...)
-
-            # TODO: merge rest of beta approx implementation in
-            # This is to ensure compatibility when we merge beta approx support in...!
-
-            # radius_effective is always s_params[0]
-
-            # if radius_effective_mode is in all_params, then all_params contains radius_effective and we want to
-            # keep it in the model
-
-            # if radius_effective_mode is NOT in all_params, then radius_effective should NOT be kept, because the user
-            # cannot specify it themselves; but, make sure we only remove it if it's actually there in the first place
-            # (sasmodels master removes it already)
+            # conflicting names with P(Q) params will cause a rename
+
             if "radius_effective_mode" in all_param_names:
                 # Show all parameters
@@ -2132,23 +2123,15 @@
                     s_params = modelinfo.ParameterTable(all_params[p_pars_len:p_pars_len+s_pars_len-1])
 
-            # Get new rows for QModel
-            # Any renamed parameters are stored as data in the relevant item, for later handling
-            new_rows = FittingUtilities.addSimpleParametersToModel(s_params, self.is2D, s_params_orig)
-
-            # TODO: merge rest of beta approx implementation in
-            # These parameters are not part of P(Q) nor S(Q), but are added only to the product model (e.g. specifying
-            # structure factor calculation mode)
-            # product_params = all_params[p_pars_len+s_pars_len:]
-
         # Add heading row
         FittingUtilities.addHeadingRowToModel(self._model_model, structure_factor)
 
-        # Update QModel
-        for row in new_rows:
-            self._model_model.appendRow(row)
-            # disable fitting of parameters not listed in self.kernel_module (probably radius_effective)
-            # if row[0].text() not in self.kernel_module.params.keys():
-            #     row_num = self._model_model.rowCount() - 1
-            #     FittingUtilities.markParameterDisabled(self._model_model, row_num)
+        # Get new rows for QModel
+        # Any renamed parameters are stored as data in the relevant item, for later handling
+        FittingUtilities.addSimpleParametersToModel(
+                s_params,
+                self.is2D,
+                s_params_orig,
+                self._model_model,
+                self.lstParams)
 
     def haveParamsToFit(self):
@@ -2813,7 +2796,12 @@
             self._model_model.removeRows(first_row, remove_rows)
 
-        new_rows = FittingUtilities.addShellsToModel(self.model_parameters, self._model_model, index, first_row)
+        new_rows = FittingUtilities.addShellsToModel(
+                self.model_parameters,
+                self._model_model,
+                index,
+                first_row,
+                self.lstParams)
+
         self._num_shell_params = len(new_rows)
-
         self.current_shell_displayed = index
 
