Index: src/sas/qtgui/Perspectives/Fitting/FitPage.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FitPage.py	(revision 2add354096316e3381348cc6317654bf004353c2)
+++ src/sas/qtgui/Perspectives/Fitting/FitPage.py	(revision 2add354096316e3381348cc6317654bf004353c2)
@@ -0,0 +1,46 @@
+class FitPage(object):
+    """
+    Container for all data related to the current fit page
+    """
+    MIN_RANGE=0
+    MAX_RANGE=1
+    NPTS=2
+    NPTS_FIT=3
+    LOG_POINTS=4
+    WEIGHTING=5
+    SMEARING_OPTION=6
+    SMEARING_ACCURACY=7
+    SMEARING_MIN=8
+    SMEARING_MAX=9
+    def __init__(self, parent=None):
+        """
+        Define the dictionary
+        """
+        # Main tab
+        self.current_category = ""
+        self.current_model = ""
+        self.current_factor = ""
+
+        self.page_id = 0
+        self.is_data_loaded = False
+        self.filename = ""
+        
+        # QModels
+        self.param_model = None
+        self.poly_model = None
+        self.magnetism_model = None
+
+        # Displayed values - tab #1
+        self.is_polydisperse = False
+        self.is2D = False
+        self.is_magnetism = False
+        self.displayed_values = {}
+        self.chi2 = None
+
+        # Fit options - tab #2
+        self.fit_options = {}
+        # Weight options - tab #2
+        #self.weighting_options = {}
+        # Smearing options - tab #3
+        self.smearing_options = {}
+
Index: src/sas/qtgui/Perspectives/Fitting/FittingLogic.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingLogic.py	(revision 180bd545166c0e0d674354e554dec53998b492c6)
+++ src/sas/qtgui/Perspectives/Fitting/FittingLogic.py	(revision 2add354096316e3381348cc6317654bf004353c2)
@@ -1,3 +1,3 @@
-import numpy
+import numpy as np
 
 from sas.sasgui.guiframe.dataFitting import Data1D
@@ -72,9 +72,9 @@
         ymin = -qmax
 
-        x = numpy.linspace(start=xmin, stop=xmax, num=qstep, endpoint=True)
-        y = numpy.linspace(start=ymin, stop=ymax, num=qstep, endpoint=True)
+        x = np.linspace(start=xmin, stop=xmax, num=qstep, endpoint=True)
+        y = np.linspace(start=ymin, stop=ymax, num=qstep, endpoint=True)
         # Use data info instead
-        new_x = numpy.tile(x, (len(y), 1))
-        new_y = numpy.tile(y, (len(x), 1))
+        new_x = np.tile(x, (len(y), 1))
+        new_y = np.tile(y, (len(x), 1))
         new_y = new_y.swapaxes(0, 1)
 
@@ -82,8 +82,8 @@
         qx_data = new_x.flatten()
         qy_data = new_y.flatten()
-        q_data = numpy.sqrt(qx_data * qx_data + qy_data * qy_data)
+        q_data = np.sqrt(qx_data * qx_data + qy_data * qy_data)
 
         # set all True (standing for unmasked) as default
-        mask = numpy.ones(len(qx_data), dtype=bool)
+        mask = np.ones(len(qx_data), dtype=bool)
         # calculate the range of qx and qy: this way,
         # it is a little more independent
@@ -93,6 +93,6 @@
 
         self._data.source = Source()
-        self._data.data = numpy.ones(len(mask))
-        self._data.err_data = numpy.ones(len(mask))
+        self._data.data = np.ones(len(mask))
+        self._data.err_data = np.ones(len(mask))
         self._data.qx_data = qx_data
         self._data.qy_data = qy_data
@@ -120,5 +120,5 @@
         new_plot = Data1D(x=x, y=y)
         new_plot.is_data = False
-        new_plot.dy = numpy.zeros(len(y))
+        new_plot.dy = np.zeros(len(y))
         _yaxis, _yunit = data.get_yaxis()
         _xaxis, _xunit = data.get_xaxis()
@@ -141,5 +141,5 @@
         update_chisqr, source = return_data
 
-        numpy.nan_to_num(image)
+        np.nan_to_num(image)
         new_plot = Data2D(image=image, err_image=data.err_data)
         new_plot.name = model.name + '2d'
@@ -194,11 +194,11 @@
             qmin = 0
             try:
-                x = max(numpy.fabs(self.data.xmin), numpy.fabs(self.data.xmax))
-                y = max(numpy.fabs(self.data.ymin), numpy.fabs(self.data.ymax))
+                x = max(np.fabs(self.data.xmin), np.fabs(self.data.xmax))
+                y = max(np.fabs(self.data.ymin), np.fabs(self.data.ymax))
             except (ValueError, TypeError):
                 msg = "Unable to find min/max of \n data named %s" % \
                             self.data.filename
                 raise ValueError, msg
-            qmax = numpy.sqrt(x * x + y * y)
+            qmax = np.sqrt(x * x + y * y)
             npts = len(self.data.data)
         return qmin, qmax, npts
Index: src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py	(revision 31e4bb8550ff812c24a1c5de4ed070404c3d892a)
+++ src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py	(revision 2add354096316e3381348cc6317654bf004353c2)
@@ -61,4 +61,7 @@
         Overwrite QDialog close method to allow for custom widget close
         """
+        # Invoke fit page events
+        for tab in self.tabs:
+            tab.close()
         if self._allow_close:
             # reset the closability flag
@@ -66,7 +69,7 @@
             event.accept()
         else:
-            event.ignore()
             # Maybe we should just minimize
             self.setWindowState(QtCore.Qt.WindowMinimized)
+            event.ignore()
 
     def addFit(self, data):
Index: src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py	(revision d48cc19810059080c3b51d9b2c82196981cbfe1f)
+++ src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py	(revision 2add354096316e3381348cc6317654bf004353c2)
@@ -56,9 +56,12 @@
         item1 = QtGui.QStandardItem(item_name)
         item1.setCheckable(True)
+        item1.setEditable(False)
         item_err = QtGui.QStandardItem()
         # check for polydisp params
         if param.polydisperse:
             poly_item = QtGui.QStandardItem("Polydispersity")
+            poly_item.setEditable(False)
             item1_1 = QtGui.QStandardItem("Distribution")
+            item1_1.setEditable(False)
             # Find param in volume_params
             for p in parameters.form_volume_parameters:
@@ -66,7 +69,11 @@
                     continue
                 item1_2 = QtGui.QStandardItem(str(p.default))
+                item1_2.setEditable(False)
                 item1_3 = QtGui.QStandardItem(str(p.limits[0]))
+                item1_3.setEditable(False)
                 item1_4 = QtGui.QStandardItem(str(p.limits[1]))
+                item1_4.setEditable(False)
                 item1_5 = QtGui.QStandardItem(p.units)
+                item1_5.setEditable(False)
                 poly_item.appendRow([item1_1, item1_2, item1_3, item1_4, item1_5])
                 break
@@ -81,4 +88,5 @@
         item4 = QtGui.QStandardItem(str(param.limits[1]))
         item5 = QtGui.QStandardItem(param.units)
+        item5.setEditable(False)
         item.append([item1, item2, item3, item4, item5])
     return item
@@ -95,12 +103,12 @@
         item1 = QtGui.QStandardItem(item_name)
         item1.setCheckable(True)
+        item1.setEditable(False)
         # Param values
+        # TODO: add delegate for validation of cells
         item2 = QtGui.QStandardItem(str(param.default))
-        # TODO: the error column.
-        # Either add a proxy model or a custom view delegate
-        item_err = QtGui.QStandardItem()
         item4 = QtGui.QStandardItem(str(param.limits[0]))
         item5 = QtGui.QStandardItem(str(param.limits[1]))
         item6 = QtGui.QStandardItem(param.units)
+        item6.setEditable(False)
         item.append([item1, item2, item4, item5, item6])
     return item
Index: src/sas/qtgui/Perspectives/Fitting/FittingWidget.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision 02ddfb44fa775a4f93bdbd0c9348ecf3a3b0b60b)
+++ src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision 2add354096316e3381348cc6317654bf004353c2)
@@ -12,4 +12,5 @@
 from PyQt4 import QtGui
 from PyQt4 import QtCore
+from PyQt4 import QtWebKit
 
 from sasmodels import generate
@@ -32,4 +33,5 @@
 from SmearingWidget import SmearingWidget
 from OptionsWidget import OptionsWidget
+from FitPage import FitPage
 
 TAB_MAGNETISM = 4
@@ -73,4 +75,5 @@
         self.log_points = False
         self.weighting = 0
+        self.chi2 = None
 
         # Data for chosen model
@@ -132,4 +135,6 @@
         """
         self.lstParams.setStyleSheet(stylesheet)
+        self.lstParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+        self.lstParams.customContextMenuRequested.connect(self.showModelDescription)
 
         # Poly model displayed in poly list
@@ -163,7 +168,17 @@
         self.initializeControls()
 
+        # Display HTML content
+        self.helpView = QtWebKit.QWebView()
+
         self._index = None
         if data is not None:
             self.data = data
+
+    def close(self):
+        """
+        Remember to kill off things on exit
+        """
+        self.helpView.close()
+        del self.helpView
 
     @property
@@ -197,5 +212,4 @@
         self.lblFilename.setText(self.logic.data.filename)
         self.updateQRange()
-        self.cmdFit.setEnabled(True)
         # Switch off Data2D control
         self.chk2DView.setEnabled(False)
@@ -285,4 +299,5 @@
         self.cmdFit.clicked.connect(self.onFit)
         self.cmdPlot.clicked.connect(self.onPlot)
+        self.cmdHelp.clicked.connect(self.onHelp)
 
         # Respond to change in parameters from the UI
@@ -294,4 +309,25 @@
         # Signals from separate tabs asking for replot
         self.options_widget.plot_signal.connect(self.onOptionsUpdate)
+
+    def showModelDescription(self, position):
+        """
+        Shows a window with model description, when right clicked in the treeview
+        """
+        msg = 'Model description:\n'
+        info = "Info"
+        if self.kernel_module is not None:
+            if str(self.kernel_module.description).rstrip().lstrip() == '':
+                msg += "Sorry, no information is available for this model."
+            else:
+                msg += self.kernel_module.description + '\n'
+        else:
+            msg += "You must select a model to get information on this"
+
+        menu = QtGui.QMenu()
+        label = QtGui.QLabel(msg)
+        action = QtGui.QWidgetAction(self)
+        action.setDefaultWidget(label)
+        menu.addAction(action)
+        menu.exec_(self.lstParams.viewport().mapToGlobal(position))
 
     def onSelectModel(self):
@@ -321,4 +357,6 @@
             # Create default datasets if no data passed
             self.createDefaultDataset()
+
+        #state = self.currentState()
 
     def onSelectStructureFactor(self):
@@ -403,4 +441,13 @@
         pass # debug anchor
 
+    def onHelp(self):
+        """
+        Show the "Fitting" section of help
+        """
+        tree_location = self.parent.HELP_DIRECTORY_LOCATION +\
+            "/user/sasgui/perspectives/fitting/fitting_help.html"
+        self.helpView.load(QtCore.QUrl(tree_location))
+        self.helpView.show()
+
     def onFit(self):
         """
@@ -438,4 +485,5 @@
         fitter.set_model(model, fit_id, params_to_fit, data=data,
                          constraints=constraints)
+
         fitter.set_data(data=data, id=fit_id, smearer=smearer, qmin=qmin,
                         qmax=qmax)
@@ -501,5 +549,5 @@
         self.communicate.statusBarUpdateSignal.emit(msg)
 
-        fitness = res.fitness
+        self.chi2 = res.fitness
         param_list = res.param_list
         param_values = res.pvec
@@ -516,5 +564,5 @@
 
         # Read only value - we can get away by just printing it here
-        chi2_repr = GuiUtils.formatNumber(fitness, high=True)
+        chi2_repr = GuiUtils.formatNumber(self.chi2, high=True)
         self.lblChi2Value.setText(chi2_repr)
 
@@ -586,5 +634,4 @@
         # Regardless of previous state, this should now be `plot show` functionality only
         self.cmdPlot.setText("Show Plot")
-        self.recalculatePlotData()
         self.showPlot()
 
@@ -685,4 +732,7 @@
         checked_list = ['background', '0.001', '-inf', 'inf', '1/cm']
         FittingUtilities.addCheckedListToModel(model, checked_list)
+        last_row = model.rowCount()-1
+        model.item(last_row, 0).setEditable(False)
+        model.item(last_row, 4).setEditable(False)
 
     def addScaleToModel(self, model):
@@ -693,4 +743,7 @@
         checked_list = ['scale', '1.0', '0.0', 'inf', '']
         FittingUtilities.addCheckedListToModel(model, checked_list)
+        last_row = model.rowCount()-1
+        model.item(last_row, 0).setEditable(False)
+        model.item(last_row, 4).setEditable(False)
 
     def addWeightingToData(self, data):
@@ -781,4 +834,5 @@
         if model_column == 0:
             self.checkboxSelected(item)
+            self.cmdFit.setEnabled(self.parameters_to_fit != [] and self.logic.data_is_loaded)
             return
 
@@ -787,5 +841,10 @@
 
         # Extract changed value. Assumes proper validation by QValidator/Delegate
-        value = float(item.text())
+        # TODO: disable model update for uneditable cells/columns
+        try:
+            value = float(item.text())
+        except ValueError:
+            # Unparsable field
+            return
         parameter_name = str(self._model_model.data(name_index).toPyObject()) # sld, background etc.
         property_name = str(self._model_model.headerData(1, model_column).toPyObject()) # Value, min, max, etc.
@@ -944,7 +1003,7 @@
         self.createNewIndex(fitted_data)
         # Calculate difference between return_data and logic.data
-        chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
+        self.chi2 = FittingUtilities.calculateChi2(fitted_data, self.logic.data)
         # Update the control
-        chi2_repr = "---" if chi2 is None else GuiUtils.formatNumber(chi2, high=True)
+        chi2_repr = "---" if self.chi2 is None else GuiUtils.formatNumber(self.chi2, high=True)
         self.lblChi2Value.setText(chi2_repr)
 
Index: src/sas/qtgui/Perspectives/Fitting/UI/FittingWidgetUI.ui
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/UI/FittingWidgetUI.ui	(revision 180bd545166c0e0d674354e554dec53998b492c6)
+++ src/sas/qtgui/Perspectives/Fitting/UI/FittingWidgetUI.ui	(revision 2add354096316e3381348cc6317654bf004353c2)
@@ -158,11 +158,23 @@
             </item>
             <item row="1" column="0">
-             <widget class="QComboBox" name="cbCategory"/>
+             <widget class="QComboBox" name="cbCategory">
+              <property name="toolTip">
+               <string>Select a category</string>
+              </property>
+             </widget>
             </item>
             <item row="1" column="1">
-             <widget class="QComboBox" name="cbModel"/>
+             <widget class="QComboBox" name="cbModel">
+              <property name="toolTip">
+               <string>Select a model</string>
+              </property>
+             </widget>
             </item>
             <item row="1" column="2">
-             <widget class="QComboBox" name="cbStructureFactor"/>
+             <widget class="QComboBox" name="cbStructureFactor">
+              <property name="toolTip">
+               <string>Select a structure factor</string>
+              </property>
+             </widget>
             </item>
            </layout>
@@ -172,4 +184,7 @@
             <property name="styleSheet">
              <string notr="true"/>
+            </property>
+            <property name="editTriggers">
+             <set>QAbstractItemView::CurrentChanged|QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
             </property>
             <property name="selectionMode">
Index: src/sas/qtgui/Perspectives/Fitting/UnitTesting/FittingWidgetTest.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/UnitTesting/FittingWidgetTest.py	(revision 02ddfb44fa775a4f93bdbd0c9348ecf3a3b0b60b)
+++ src/sas/qtgui/Perspectives/Fitting/UnitTesting/FittingWidgetTest.py	(revision 2add354096316e3381348cc6317654bf004353c2)
@@ -18,4 +18,5 @@
 
 from sas.sasgui.guiframe.dataFitting import Data1D
+from sas.sasgui.guiframe.dataFitting import Data2D
 
 app = QtGui.QApplication(sys.argv)
@@ -50,5 +51,5 @@
         self.assertFalse(self.widget.data_is_loaded)
 
-    def testSelectCategory(self):
+    def testSelectCategoryDefault(self):
         """
         Test if model categories have been loaded properly
@@ -79,42 +80,4 @@
         # self.assertTrue(widget_with_data.cmdFit.isEnabled())
         self.assertFalse(widget_with_data.acceptsData())
-
-    def testSelectModel(self):
-        """
-        Test if models have been loaded properly
-        """
-        fittingWindow =  self.widget
-
-        #Test loading from json categories
-        model_list = fittingWindow.master_category_dict["Cylinder"]
-        self.assertTrue(['cylinder', True] in model_list)
-        self.assertTrue(['core_shell_cylinder', True] in model_list)
-        self.assertTrue(['barbell', True] in model_list)
-        self.assertTrue(['core_shell_bicelle', True] in model_list)
-        self.assertTrue(['flexible_cylinder', True] in model_list)
-        self.assertTrue(['flexible_cylinder_elliptical', True] in model_list)
-        self.assertTrue(['pearl_necklace', True] in model_list)
-        self.assertTrue(['capped_cylinder', True] in model_list)
-        self.assertTrue(['elliptical_cylinder', True] in model_list)
-        self.assertTrue(['pringle', True] in model_list)
-        self.assertTrue(['hollow_cylinder', True] in model_list)
-        self.assertTrue(['core_shell_bicelle_elliptical', True] in model_list)
-        self.assertTrue(['stacked_disks', True] in model_list)
-
-        #Test for existence in combobox
-        self.assertNotEqual(fittingWindow.cbModel.findText("cylinder"),-1)
-        self.assertNotEqual(fittingWindow.cbModel.findText("core_shell_cylinder"),-1)
-        self.assertNotEqual(fittingWindow.cbModel.findText("barbell"),-1)
-        self.assertNotEqual(fittingWindow.cbModel.findText("core_shell_bicelle"),-1)
-        self.assertNotEqual(fittingWindow.cbModel.findText("flexible_cylinder"),-1)
-        self.assertNotEqual(fittingWindow.cbModel.findText("flexible_cylinder_elliptical"),-1)
-        self.assertNotEqual(fittingWindow.cbModel.findText("pearl_necklace"),-1)
-        self.assertNotEqual(fittingWindow.cbModel.findText("capped_cylinder"),-1)
-        self.assertNotEqual(fittingWindow.cbModel.findText("elliptical_cylinder"),-1)
-        self.assertNotEqual(fittingWindow.cbModel.findText("pringle"),-1)
-        self.assertNotEqual(fittingWindow.cbModel.findText("hollow_cylinder"),-1)
-        self.assertNotEqual(fittingWindow.cbModel.findText("core_shell_bicelle_elliptical"),-1)
-        self.assertNotEqual(fittingWindow.cbModel.findText("stacked_disks"),-1)
-
 
     def testSelectPolydispersity(self):
@@ -154,5 +117,5 @@
         pass
 
-    def  testSelectCategory(self):
+    def testSelectCategory(self):
         """
         Assure proper behaviour on changing category
@@ -498,5 +461,5 @@
         self.assertEqual(spy.count(), 1)
 
-    def testOnFit(self):
+    def testOnFit1D(self):
         """
         Test the threaded fitting call
@@ -540,4 +503,51 @@
             self.assertEqual(update_spy.count(), 1)
 
+    def testOnFit2D(self):
+        """
+        Test the threaded fitting call
+        """
+        # Set data
+        test_data = Data2D(image=[1.0, 2.0, 3.0],
+                           err_image=[0.01, 0.02, 0.03],
+                           qx_data=[0.1, 0.2, 0.3],
+                           qy_data=[0.1, 0.2, 0.3],
+                           xmin=0.1, xmax=0.3, ymin=0.1, ymax=0.3,
+                           mask=[True, True, True])
+
+        # Force same data into logic
+        self.widget.logic.data = test_data
+        self.widget.data_is_loaded = True
+        category_index = self.widget.cbCategory.findText("Sphere")
+        self.widget.cbCategory.setCurrentIndex(category_index)
+
+        self.widget.show()
+
+        # Test no fitting params
+        self.widget.parameters_to_fit = []
+
+        with self.assertRaises(ValueError) as error:
+            self.widget.onFit()
+        self.assertEqual(str(error.exception), 'no fitting parameters')
+
+        # Assing fitting params
+        self.widget.parameters_to_fit = ['scale']
+
+        # Spying on status update signal
+        update_spy = QtSignalSpy(self.widget, self.widget.communicate.statusBarUpdateSignal)
+
+        with threads.deferToThread as MagicMock:
+            self.widget.onFit()
+            # thread called
+            self.assertTrue(threads.deferToThread.called)
+            # thread method is 'compute'
+            self.assertEqual(threads.deferToThread.call_args_list[0][0][0].__name__, 'compute')
+
+            # the fit button changed caption and got disabled
+            self.assertEqual(self.widget.cmdFit.text(), 'Calculating...')
+            self.assertFalse(self.widget.cmdFit.isEnabled())
+
+            # Signal pushed up
+            self.assertEqual(update_spy.count(), 1)
+
 
 if __name__ == "__main__":
