Index: src/sas/qtgui/MainWindow/DataExplorer.py
===================================================================
--- src/sas/qtgui/MainWindow/DataExplorer.py	(revision d6e3866160051b03e5bdb581acf0fd2227b090b4)
+++ src/sas/qtgui/MainWindow/DataExplorer.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -476,4 +476,11 @@
         self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
 
+    def itemFromFilename(self, filename):
+        """
+        Retrieves model item corresponding to the given filename
+        """
+        item = GuiUtils.itemFromFilename(filename, self.model)
+        return item
+
     def displayFile(self, filename=None, is_data=True):
         """
Index: src/sas/qtgui/MainWindow/GuiManager.py
===================================================================
--- src/sas/qtgui/MainWindow/GuiManager.py	(revision 3b8cc00520d905f403798a463444af479d8bdd87)
+++ src/sas/qtgui/MainWindow/GuiManager.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -21,4 +21,6 @@
 from sas.qtgui.Utilities.TabbedModelEditor import TabbedModelEditor
 from sas.qtgui.Utilities.PluginManager import PluginManager
+from sas.qtgui.Utilities.GridPanel import BatchOutputPanel
+
 from sas.qtgui.MainWindow.UI.AcknowledgementsUI import Ui_Acknowledgements
 from sas.qtgui.MainWindow.AboutBox import AboutBox
@@ -130,4 +132,5 @@
         self.aboutWidget = AboutBox()
         self.welcomePanel = WelcomePanel()
+        self.grid_window = None
 
         # Add calculators - floating for usability
@@ -263,4 +266,10 @@
             msg = "Guiframe does not have a current perspective"
             logging.info(msg)
+
+    def findItemFromFilename(self, filename):
+        """
+        Queries the data explorer for the index corresponding to the filename within
+        """
+        return self.filesWidget.itemFromFilename(filename)
 
     def quitApplication(self):
@@ -405,5 +414,4 @@
         self._workspace.actionGPU_Options.triggered.connect(self.actionGPU_Options)
         self._workspace.actionFit_Results.triggered.connect(self.actionFit_Results)
-        self._workspace.actionChain_Fitting.triggered.connect(self.actionChain_Fitting)
         self._workspace.actionAdd_Custom_Model.triggered.connect(self.actionAdd_Custom_Model)
         self._workspace.actionEdit_Custom_Model.triggered.connect(self.actionEdit_Custom_Model)
@@ -428,4 +436,6 @@
         self._workspace.actionCheck_for_update.triggered.connect(self.actionCheck_for_update)
 
+        self.communicate.sendDataToGridSignal.connect(self.showBatchOutput)
+
     #============ FILE =================
     def actionLoadData(self):
@@ -527,6 +537,22 @@
         """
         """
-        print("actionShow_Grid_Window TRIGGERED")
-        pass
+        self.showBatchOutput(None)
+
+    def showBatchOutput(self, output_data):
+        """
+        Display/redisplay the batch fit viewer
+        """
+        if self.grid_window is None:
+            self.grid_window = BatchOutputPanel(parent=self, output_data=output_data)
+            subwindow = self._workspace.workspace.addSubWindow(self.grid_window)
+
+            #self.grid_window = BatchOutputPanel(parent=self, output_data=output_data)
+            self.grid_window.show()
+            return
+        if output_data:
+            self.grid_window.addFitResults(output_data)
+        self.grid_window.show()
+        if self.grid_window.windowState() == Qt.WindowMinimized:
+            self.grid_window.setWindowState(Qt.WindowActive)
 
     def actionHide_Toolbar(self):
@@ -658,10 +684,4 @@
         """
         print("actionFit_Results TRIGGERED")
-        pass
-
-    def actionChain_Fitting(self):
-        """
-        """
-        print("actionChain_Fitting TRIGGERED")
         pass
 
Index: src/sas/qtgui/MainWindow/UI/MainWindowUI.ui
===================================================================
--- src/sas/qtgui/MainWindow/UI/MainWindowUI.ui	(revision 01ef3f797e8d4032a84fb98c0186de9326036df9)
+++ src/sas/qtgui/MainWindow/UI/MainWindowUI.ui	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -106,6 +106,4 @@
     <addaction name="actionFit_Results"/>
     <addaction name="separator"/>
-    <addaction name="actionChain_Fitting"/>
-    <addaction name="separator"/>
     <addaction name="actionAdd_Custom_Model"/>
     <addaction name="actionEdit_Custom_Model"/>
Index: src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py	(revision 8b480d271418f2fa35e0597730c97807da3140d6)
+++ src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -358,6 +358,11 @@
         elapsed = result[1]
 
-        # ADD THE BATCH FIT VIEW HERE
-        #
+        if result is None:
+            msg = "Fitting failed."
+            self.parent.communicate.statusBarUpdateSignal.emit(msg)
+            return
+
+        # Show the grid panel
+        self.parent.communicate.sendDataToGridSignal.emit(result[0])
 
         msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
Index: src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py	(revision fde5bcd7ef61030233f0bcfbd1ff0923cdb9a925)
+++ src/sas/qtgui/Perspectives/Fitting/FittingUtilities.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -421,2 +421,18 @@
         weight = numpy.abs(data)
     return weight
+
+def updateKernelWithResults(kernel, results):
+    """
+    Takes model kernel and applies results dict to its parameters,
+    returning the modified (deep) copy of the kernel.
+    """
+    assert(isinstance(results, dict))
+    local_kernel = copy.deepcopy(kernel)
+
+    for parameter in results.keys():
+        # Update the parameter value - note: this supports +/-inf as well
+        local_kernel.setParam(parameter, results[parameter][0])
+
+    return local_kernel
+
+
Index: src/sas/qtgui/Perspectives/Fitting/FittingWidget.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision ded5e77dee7514710580502d488f33dd8b6acc19)
+++ src/sas/qtgui/Perspectives/Fitting/FittingWidget.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -3,5 +3,5 @@
 from collections import defaultdict
 
-
+import copy
 import logging
 import traceback
@@ -24,5 +24,4 @@
 import sas.qtgui.Utilities.GuiUtils as GuiUtils
 import sas.qtgui.Utilities.LocalConfig as LocalConfig
-from sas.qtgui.Utilities.GridPanel import BatchOutputPanel
 from sas.qtgui.Utilities.CategoryInstaller import CategoryInstaller
 from sas.qtgui.Plotting.PlotterData import Data1D
@@ -88,4 +87,6 @@
     fittingFinishedSignal = QtCore.pyqtSignal(tuple)
     batchFittingFinishedSignal = QtCore.pyqtSignal(tuple)
+    Calc1DFinishedSignal = QtCore.pyqtSignal(tuple)
+    Calc2DFinishedSignal = QtCore.pyqtSignal(tuple)
 
     def __init__(self, parent=None, data=None, tab_id=1):
@@ -99,7 +100,4 @@
         self.tab_id = tab_id
 
-        # Main Data[12]D holder
-        self.logic = FittingLogic()
-
         # Globals
         self.initializeGlobals()
@@ -107,4 +105,10 @@
         # Set up desired logging level
         logging.disable(LocalConfig.DISABLE_LOGGING)
+
+        # data index for the batch set
+        self.data_index = 0
+        # Main Data[12]D holders
+        # Logics.data contains a single Data1D/Data2D object
+        self._logic = [FittingLogic()]
 
         # Main GUI setup up
@@ -134,6 +138,6 @@
         self.initializeControls()
 
-        # Display HTML content
-        #self.setupHelp()
+        if data is not None:
+            self.data = data
 
         # New font to display angstrom symbol
@@ -142,7 +146,10 @@
         self.label_19.setStyleSheet(new_font)
 
-        self._index = None
-        if data is not None:
-            self.data = data
+    @property
+    def logic(self):
+        # make sure the logic contains at least one element
+        assert(self._logic)
+        # logic connected to the currently shown data
+        return self._logic[self.data_index]
 
     @property
@@ -161,13 +168,14 @@
 
         assert isinstance(value[0], QtGui.QStandardItem)
-        # _index contains the QIndex with data
-        self._index = value[0]
 
         # Keep reference to all datasets for batch
         self.all_data = value
 
-        # Update logics with data items
+        # Create logics with data items
+        self._logic=[]
         # Logics.data contains only a single Data1D/Data2D object
-        self.logic.data = GuiUtils.dataFromItem(value[0])
+        for data_item in value:
+            self._logic.append(FittingLogic())
+            self._logic[-1].data = GuiUtils.dataFromItem(data_item)
 
         # Overwrite data type descriptor
@@ -226,4 +234,6 @@
         # Polydisp widget table default index for function combobox
         self.orig_poly_index = 3
+        # copy of current kernel model
+        self.kernel_module_copy = None
 
         # Page id for fitting
@@ -488,4 +498,6 @@
         self.batchFittingFinishedSignal.connect(self.batchFitComplete)
         self.fittingFinishedSignal.connect(self.fitComplete)
+        self.Calc1DFinishedSignal.connect(self.complete1D)
+        self.Calc2DFinishedSignal.connect(self.complete2D)
 
         # Signals from separate tabs asking for replot
@@ -930,6 +942,5 @@
         Update the logic based on the selected file in batch fitting
         """
-        self._index = self.all_data[data_index]
-        self.logic.data = GuiUtils.dataFromItem(self.all_data[data_index])
+        self.data_index = data_index
         self.updateQRange()
 
@@ -1231,4 +1242,7 @@
             return
 
+        # keep local copy of kernel parameters, as they will change during the update
+        self.kernel_module_copy = copy.deepcopy(self.kernel_module)
+
         # Create the fitting thread, based on the fitter
         completefn = self.batchFittingCompleted if self.is_batch_fitting else self.fittingCompleted
@@ -1297,21 +1311,4 @@
         #re-enable the Fit button
         self.setFittingStopped()
-        # Show the grid panel
-        self.grid_window = BatchOutputPanel(parent=self, output_data=result[0])
-        self.grid_window.show()
-
-    def fittingCompleted(self, result):
-        """
-        Send the finish message from calculate threads to main thread
-        """
-        self.fittingFinishedSignal.emit(result)
-
-    def fitComplete(self, result):
-        """
-        Receive and display fitting results
-        "result" is a tuple of actual result list and the fit time in seconds
-        """
-        #re-enable the Fit button
-        self.setFittingStopped()
 
         if result is None:
@@ -1320,15 +1317,78 @@
             return
 
+        # Show the grid panel
+        self.communicate.sendDataToGridSignal.emit(result[0])
+
+        elapsed = result[1]
+        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
+        self.communicate.statusBarUpdateSignal.emit(msg)
+
+        # Run over the list of results and update the items
+        for res_index, res_list in enumerate(result[0]):
+            # results
+            res = res_list[0]
+            param_dict = self.paramDictFromResults(res)
+
+            # create local kernel_module
+            kernel_module = FittingUtilities.updateKernelWithResults(self.kernel_module, param_dict)
+            # pull out current data
+            data = self._logic[res_index].data
+
+            # Switch indexes
+            self.onSelectBatchFilename(res_index)
+
+            method = self.complete1D if isinstance(self.data, Data1D) else self.complete2D
+            self.calculateQGridForModelExt(data=data, model=kernel_module, completefn=method, use_threads=False)
+
+        # Restore original kernel_module, so subsequent fits on the same model don't pick up the new params
+        if self.kernel_module is not None:
+            self.kernel_module = copy.deepcopy(self.kernel_module_copy)
+
+    def paramDictFromResults(self, results):
+        """
+        Given the fit results structure, pull out optimized parameters and return them as nicely
+        formatted dict
+        """
+        if results.fitness is None or \
+            not np.isfinite(results.fitness) or \
+            np.any(results.pvec is None) or \
+            not np.all(np.isfinite(results.pvec)):
+            msg = "Fitting did not converge!"
+            self.communicate.statusBarUpdateSignal.emit(msg)
+            msg += results.mesg
+            logging.error(msg)
+            return
+
+        param_list = results.param_list # ['radius', 'radius.width']
+        param_values = results.pvec     # array([ 0.36221662,  0.0146783 ])
+        param_stderr = results.stderr   # array([ 1.71293015,  1.71294233])
+        params_and_errors = list(zip(param_values, param_stderr))
+        param_dict = dict(zip(param_list, params_and_errors))
+
+        return param_dict
+
+    def fittingCompleted(self, result):
+        """
+        Send the finish message from calculate threads to main thread
+        """
+        self.fittingFinishedSignal.emit(result)
+
+    def fitComplete(self, result):
+        """
+        Receive and display fitting results
+        "result" is a tuple of actual result list and the fit time in seconds
+        """
+        #re-enable the Fit button
+        self.setFittingStopped()
+
+        if result is None:
+            msg = "Fitting failed."
+            self.communicate.statusBarUpdateSignal.emit(msg)
+            return
+
         res_list = result[0][0]
         res = res_list[0]
-        if res.fitness is None or \
-            not np.isfinite(res.fitness) or \
-            np.any(res.pvec is None) or \
-            not np.all(np.isfinite(res.pvec)):
-            msg = "Fitting did not converge!"
-            self.communicate.statusBarUpdateSignal.emit(msg)
-            msg += res.mesg
-            logging.error(msg)
-            return
+        self.chi2 = res.fitness
+        param_dict = self.paramDictFromResults(res)
 
         elapsed = result[1]
@@ -1339,11 +1399,4 @@
             msg = "Fitting completed successfully in: %s s." % GuiUtils.formatNumber(elapsed)
         self.communicate.statusBarUpdateSignal.emit(msg)
-
-        self.chi2 = res.fitness
-        param_list = res.param_list # ['radius', 'radius.width']
-        param_values = res.pvec     # array([ 0.36221662,  0.0146783 ])
-        param_stderr = res.stderr   # array([ 1.71293015,  1.71294233])
-        params_and_errors = list(zip(param_values, param_stderr))
-        param_dict = dict(zip(param_list, params_and_errors))
 
         # Dictionary of fitted parameter: value, error
@@ -2001,5 +2054,5 @@
             fitted_data.symbol = 'Line'
         # Notify the GUI manager so it can update the main model in DataExplorer
-        GuiUtils.updateModelItemWithPlot(self._index, fitted_data, name)
+        GuiUtils.updateModelItemWithPlot(self.all_data[self.data_index], fitted_data, name)
 
     def createTheoryIndex(self, fitted_data):
@@ -2031,15 +2084,20 @@
     def methodCompleteForData(self):
         '''return the method for result parsin on calc complete '''
-        return self.complete1D if isinstance(self.data, Data1D) else self.complete2D
-
-    def calculateQGridForModel(self):
-        """
-        Prepare the fitting data object, based on current ModelModel
-        """
-        if self.kernel_module is None:
-            return
+        return self.completed1D if isinstance(self.data, Data1D) else self.completed2D
+
+    def calculateQGridForModelExt(self, data=None, model=None, completefn=None, use_threads=True):
+        """
+        Wrapper for Calc1D/2D calls
+        """
+        if data is None:
+            data = self.data
+        if model is None:
+            model = self.kernel_module
+        if completefn is None:
+            completefn = self.methodCompleteForData()
+
         # Awful API to a backend method.
-        method = self.methodCalculateForData()(data=self.data,
-                                               model=self.kernel_module,
+        calc_thread = self.methodCalculateForData()(data=data,
+                                               model=model,
                                                page_id=0,
                                                qmin=self.q_range_min,
@@ -2050,12 +2108,29 @@
                                                fid=None,
                                                toggle_mode_on=False,
-                                               completefn=None,
+                                               completefn=completefn,
                                                update_chisqr=True,
                                                exception_handler=self.calcException,
                                                source=None)
-
-        calc_thread = threads.deferToThread(method.compute)
-        calc_thread.addCallback(self.methodCompleteForData())
-        calc_thread.addErrback(self.calculateDataFailed)
+        if use_threads:
+            if LocalConfig.USING_TWISTED:
+                # start the thread with twisted
+                thread = threads.deferToThread(calc_thread.compute)
+                thread.addCallback(completefn)
+                thread.addErrback(self.calculateDataFailed)
+            else:
+                # Use the old python threads + Queue
+                calc_thread.queue()
+                calc_thread.ready(2.5)
+        else:
+            results = calc_thread.compute()
+            completefn(results)
+
+    def calculateQGridForModel(self):
+        """
+        Prepare the fitting data object, based on current ModelModel
+        """
+        if self.kernel_module is None:
+            return
+        self.calculateQGridForModelExt()
 
     def calculateDataFailed(self, reason):
@@ -2064,4 +2139,10 @@
         """
         print("Calculate Data failed with ", reason)
+
+    def completed1D(self, return_data):
+        self.Calc1DFinishedSignal.emit(return_data)
+
+    def completed2D(self, return_data):
+        self.Calc2DFinishedSignal.emit(return_data)
 
     def complete1D(self, return_data):
@@ -2107,5 +2188,4 @@
         residuals_plot.id = "Residual " + residuals_plot.id
         self.createNewIndex(residuals_plot)
-        #self.communicate.plotUpdateSignal.emit([residuals_plot])
 
     def calcException(self, etype, value, tb):
Index: src/sas/qtgui/Perspectives/Fitting/ModelThread.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/ModelThread.py	(revision cee5c78929349ae10afc88cb8b95e0c907f1826a)
+++ src/sas/qtgui/Perspectives/Fitting/ModelThread.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -8,4 +8,5 @@
 from sas.sascalc.data_util.calcthread import CalcThread
 from sas.sascalc.fit.MultiplicationModel import MultiplicationModel
+import sas.qtgui.Utilities.LocalConfig as LocalConfig
 
 class Calc2D(CalcThread):
@@ -99,33 +100,36 @@
         output[index_model] = value
         elapsed = time.time() - self.starttime
-        #self.complete(image=output,
-        #               data=self.data,
-        #               page_id=self.page_id,
-        #               model=self.model,
-        #               state=self.state,
-        #               toggle_mode_on=self.toggle_mode_on,
-        #               elapsed=elapsed,
-        #               index=index_model,
-        #               fid=self.fid,
-        #               qmin=self.qmin,
-        #               qmax=self.qmax,
-        #               weight=self.weight,
-        #               #qstep=self.qstep,
-        #               update_chisqr=self.update_chisqr,
-        #               source=self.source)
-        return (output,
-                self.data,
-                self.page_id,
-                self.model,
-                self.state,
-                self.toggle_mode_on,
-                elapsed,
-                index_model,
-                self.fid,
-                self.qmin,
-                self.qmax,
-                self.weight,
-                self.update_chisqr,
-                self.source)
+
+        if LocalConfig.USING_TWISTED:
+            return (output,
+                    self.data,
+                    self.page_id,
+                    self.model,
+                    self.state,
+                    self.toggle_mode_on,
+                    elapsed,
+                    index_model,
+                    self.fid,
+                    self.qmin,
+                    self.qmax,
+                    self.weight,
+                    self.update_chisqr,
+                    self.source)
+        else:
+            self.complete(image=output,
+                           data=self.data,
+                           page_id=self.page_id,
+                           model=self.model,
+                           state=self.state,
+                           toggle_mode_on=self.toggle_mode_on,
+                           elapsed=elapsed,
+                           index=index_model,
+                           fid=self.fid,
+                           qmin=self.qmin,
+                           qmax=self.qmax,
+                           weight=self.weight,
+                           #qstep=self.qstep,
+                           update_chisqr=self.update_chisqr,
+                           source=self.source)
 
 
@@ -229,34 +233,31 @@
         elapsed = time.time() - self.starttime
 
-        return (self.data.x[index], output[index],
-                self.page_id,
-                self.state,
-                self.weight,
-                self.fid,
-                self.toggle_mode_on,
-                elapsed, index, self.model,
-                self.data,
-                self.update_chisqr,
-                self.source)
-
-        # TODO: as of 4.1, the output contains more items:
-        # unsmeared_* and pq_model/sq_model
-        # Need to add these too
-
-        #self.complete(x=self.data.x[index], y=output[index],
-        #              page_id=self.page_id,
-        #              state=self.state,
-        #              weight=self.weight,
-        #              fid=self.fid,
-        #              toggle_mode_on=self.toggle_mode_on,
-        #              elapsed=elapsed, index=index, model=self.model,
-        #              data=self.data,
-        #              update_chisqr=self.update_chisqr,
-        #              source=self.source,
-        #              unsmeared_model=unsmeared_output,
-        #              unsmeared_data=unsmeared_data,
-        #              unsmeared_error=unsmeared_error,
-        #              pq_model=pq_values,
-        #              sq_model=sq_values)
+        if LocalConfig.USING_TWISTED:
+            return (self.data.x[index], output[index],
+                    self.page_id,
+                    self.state,
+                    self.weight,
+                    self.fid,
+                    self.toggle_mode_on,
+                    elapsed, index, self.model,
+                    self.data,
+                    self.update_chisqr,
+                    self.source)
+        else:
+            self.complete(x=self.data.x[index], y=output[index],
+                          page_id=self.page_id,
+                          state=self.state,
+                          weight=self.weight,
+                          fid=self.fid,
+                          toggle_mode_on=self.toggle_mode_on,
+                          elapsed=elapsed, index=index, model=self.model,
+                          data=self.data,
+                          update_chisqr=self.update_chisqr,
+                          source=self.source,
+                          unsmeared_model=unsmeared_output,
+                          unsmeared_data=unsmeared_data,
+                          unsmeared_error=unsmeared_error,
+                          pq_model=pq_values,
+                          sq_model=sq_values)
 
     def results(self):
Index: src/sas/qtgui/Perspectives/Fitting/ModelUtilities.py
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/ModelUtilities.py	(revision 3b3b40bf9d5056d902b19728c621dd2a4661d65f)
+++ src/sas/qtgui/Perspectives/Fitting/ModelUtilities.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -173,8 +173,4 @@
         return {}
 
-    plugin_log("looking for models in: %s" % str(directory))
-    # compile_file(directory)  #always recompile the folder plugin
-    logging.info("plugin model dir: %s" % str(directory))
-
     plugins = {}
     for filename in os.listdir(directory):
Index: src/sas/qtgui/Perspectives/Fitting/UI/FittingWidgetUI.ui
===================================================================
--- src/sas/qtgui/Perspectives/Fitting/UI/FittingWidgetUI.ui	(revision 3b3b40bf9d5056d902b19728c621dd2a4661d65f)
+++ src/sas/qtgui/Perspectives/Fitting/UI/FittingWidgetUI.ui	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -216,5 +216,5 @@
             </property>
             <property name="toolTip">
-             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Switch on magnetic scattering parameters.&lt;/p&gt;&lt;p&gt;This option is available only for 2D models.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
+             <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Switch on Chain Fitting (parameter reuse) for batch datasets.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
             </property>
             <property name="text">
Index: src/sas/qtgui/Utilities/GridPanel.py
===================================================================
--- src/sas/qtgui/Utilities/GridPanel.py	(revision 8b480d271418f2fa35e0597730c97807da3140d6)
+++ src/sas/qtgui/Utilities/GridPanel.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -1,11 +1,12 @@
 import os
+import sys
 import time
 import logging
 import webbrowser
 
-from PyQt5 import QtCore, QtWidgets
+from PyQt5 import QtCore, QtWidgets, QtGui
 
 import sas.qtgui.Utilities.GuiUtils as GuiUtils
-
+from sas.qtgui.Plotting.PlotterData import Data1D
 from sas.qtgui.Utilities.UI.GridPanelUI import Ui_GridPanelUI
 
@@ -15,10 +16,11 @@
     Class for stateless grid-like printout of model parameters for mutiple models
     """
+    ERROR_COLUMN_CAPTION = " (Err)"
+    IS_WIN = (sys.platform == 'win32')
     def __init__(self, parent = None, output_data=None):
 
-        super(BatchOutputPanel, self).__init__()
+        super(BatchOutputPanel, self).__init__(parent._parent)
         self.setupUi(self)
 
-        self.data = output_data
         self.parent = parent
         if hasattr(self.parent, "communicate"):
@@ -30,13 +32,38 @@
         self.grid_filename = ""
 
+        self.has_data = False if output_data is None else True
+        # Tab numbering
+        self.tab_number = 1
+
+        # System dependent menu items
+        if not self.IS_WIN:
+            self.actionOpen_with_Excel.setVisible(False)
+
+        # list of QTableWidgets, indexed by tab number
+        self.tables = []
+        self.tables.append(self.tblParams)
+
         # context menu on the table
         self.tblParams.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
         self.tblParams.customContextMenuRequested.connect(self.showContextMenu)
 
-        # Fill in the table from input data
-        self.setupTable(output_data)
-
         # Command buttons
         self.cmdHelp.clicked.connect(self.onHelp)
+        self.cmdPlot.clicked.connect(self.onPlot)
+
+        # Fill in the table from input data
+        self.setupTable(widget=self.tblParams, data=output_data)
+        if output_data is not None:
+            # Set a table tooltip describing the model
+            model_name = output_data[0][0].model.id
+            self.tabWidget.setTabToolTip(0, model_name)
+
+    def closeEvent(self, event):
+        """
+        Overwrite QDialog close method to allow for custom widget close
+        """
+        # Maybe we should just minimize
+        self.setWindowState(QtCore.Qt.WindowMinimized)
+        event.ignore()
 
     def addToolbarActions(self):
@@ -64,4 +91,11 @@
 
         self.setupTableFromCSV(lines)
+        self.has_data = True
+
+    def currentTable(self):
+        """
+        Returns the currently shown QTabWidget
+        """
+        return self.tables[self.tabWidget.currentIndex()]
 
     def showContextMenu(self, position):
@@ -70,5 +104,5 @@
         """
         menu = QtWidgets.QMenu()
-        rows = [s.row() for s in self.tblParams.selectionModel().selectedRows()]
+        rows = [s.row() for s in self.currentTable().selectionModel().selectedRows()]
         num_rows = len(rows)
         if num_rows <= 0:
@@ -84,10 +118,49 @@
 
         # Define the callbacks
-        self.actionPlotResults.triggered.connect(self.plotFits)
+        self.actionPlotResults.triggered.connect(self.onPlot)
         try:
-            menu.exec_(self.tblParams.viewport().mapToGlobal(position))
+            menu.exec_(self.currentTable().viewport().mapToGlobal(position))
         except AttributeError as ex:
             logging.error("Error generating context menu: %s" % ex)
         return
+
+    def addTabPage(self):
+        """
+        Add new tab page with QTableWidget
+        """
+        layout = QtWidgets.QVBoxLayout()
+        tab_widget = QtWidgets.QTableWidget(parent=self)
+        # Same behaviour as the original tblParams
+        tab_widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
+        tab_widget.setAlternatingRowColors(True)
+        tab_widget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
+        tab_widget.setLayout(layout)
+        # Simple naming here.
+        # One would think naming the tab with current model name would be good.
+        # However, some models have LONG names, which doesn't look well on the tab bar.
+        self.tab_number += 1
+        tab_name = "Tab " + str(self.tab_number)
+        # each table needs separate slots.
+        tab_widget.customContextMenuRequested.connect(self.showContextMenu)
+        self.tables.append(tab_widget)
+        self.tabWidget.addTab(tab_widget, tab_name)
+        # Make the new tab active
+        self.tabWidget.setCurrentIndex(self.tab_number-1)
+
+    def addFitResults(self, results):
+        """
+        Create a new tab with batch fitting results
+        """
+        self.addTabPage()
+        # Update the new widget
+        # Fill in the table from input data in the last/newest page
+        assert(self.tables)
+        self.setupTable(widget=self.tables[-1], data=results)
+        self.has_data = True
+
+        # Set a table tooltip describing the model
+        model_name = results[0][0].model.id
+        self.tabWidget.setTabToolTip(self.tabWidget.count()-1, model_name)
+
 
     @classmethod
@@ -103,10 +176,14 @@
             logging.warning("Cannot display help. %s" % ex)
 
-    def plotFits(self):
+    def onPlot(self):
         """
         Plot selected fits by sending signal to the parent
         """
-        rows = [s.row() for s in self.tblParams.selectionModel().selectedRows()]
-        data = self.dataFromTable(self.tblParams)
+        rows = [s.row() for s in self.currentTable().selectionModel().selectedRows()]
+        if not rows:
+            msg = "Nothing to plot!"
+            self.parent.communicate.statusBarUpdateSignal.emit(msg)
+            return
+        data = self.dataFromTable(self.currentTable())
         # data['Data'] -> ['filename1', 'filename2', ...]
         # look for the 'Data' column and extract the filename
@@ -141,5 +218,5 @@
             tmpfile = tempfile.NamedTemporaryFile(delete=False, mode="w+", suffix=".csv")
             self.grid_filename = tmpfile.name
-            data = self.dataFromTable(self.tblParams)
+            data = self.dataFromTable(self.currentTable())
             t = time.localtime(time.time())
             time_str = time.strftime("%b %d %H:%M of %Y", t)
@@ -181,5 +258,5 @@
         if not filename:
             return
-        data = self.dataFromTable(self.tblParams)
+        data = self.dataFromTable(self.currentTable())
         details = "File generated by SasView\n"
         with open(filename, 'w') as csv_file:
@@ -190,38 +267,65 @@
         Create tablewidget items and show them, based on params
         """
-        # Clear existing display
-        self.tblParams.clear()
+        # Is this an empty grid?
+        if self.has_data:
+            # Add a new page
+            self.addTabPage()
+            # Access the newly created QTableWidget
+            current_page = self.tables[-1]
+        else:
+            current_page = self.tblParams
         # headers
         param_list = csv_data[1].rstrip().split(',')
+        # need to remove the 2 header rows to get the total data row number
+        rows = len(csv_data) -2
+        assert(rows > -1)
+        columns = len(param_list)
+        current_page.setColumnCount(columns)
+        current_page.setRowCount(rows)
+
         for i, param in enumerate(param_list):
-            self.tblParams.setHorizontalHeaderItem(i, QtWidgets.QTableWidgetItem(param))
+            current_page.setHorizontalHeaderItem(i, QtWidgets.QTableWidgetItem(param))
 
         # first - Chi2 and data filename
         for i_row, row in enumerate(csv_data[2:]):
             for i_col, col in enumerate(row.rstrip().split(',')):
-                self.tblParams.setItem(i_row, i_col, QtWidgets.QTableWidgetItem(col))
-
-        self.tblParams.resizeColumnsToContents()
-
-    def setupTable(self, data):
+                current_page.setItem(i_row, i_col, QtWidgets.QTableWidgetItem(col))
+
+        current_page.resizeColumnsToContents()
+
+    def setupTable(self, widget=None, data=None):
         """
         Create tablewidget items and show them, based on params
         """
-        # headers
+        # quietly leave is nothing to show
+        if data is None or widget is None:
+            return
+
+        # Figure out the headers
         model = data[0][0]
+
+        # TODO: add a conditional for magnetic models
         param_list = [m for m in model.model.params.keys() if ":" not in m]
 
         # Check if 2D model. If not, remove theta/phi
+        if isinstance(model.data.sas_data, Data1D):
+            param_list.remove('theta')
+            param_list.remove('phi')
 
         rows = len(data)
         columns = len(param_list)
-        self.tblParams.setColumnCount(columns+2)
-        self.tblParams.setRowCount(rows)
-
+
+        widget.setColumnCount(columns+2) # add 2 initial columns defined below
+        widget.setRowCount(rows)
+
+        # Insert two additional columns
         param_list.insert(0, "Data")
         param_list.insert(0, "Chi2")
         for i, param in enumerate(param_list):
-            self.tblParams.setHorizontalHeaderItem(i, QtWidgets.QTableWidgetItem(param))
-
+            widget.setHorizontalHeaderItem(i, QtWidgets.QTableWidgetItem(param))
+
+        # dictionary of parameter errors for post-processing
+        # [param_name] = [param_column_nr, error_for_row_1, error_for_row_2,...]
+        error_columns = {}
         # first - Chi2 and data filename
         for i_row, row in enumerate(data):
@@ -231,6 +335,6 @@
             if hasattr(row[0].data, "sas_data"):
                 filename = row[0].data.sas_data.filename
-            self.tblParams.setItem(i_row, 0, QtWidgets.QTableWidgetItem(GuiUtils.formatNumber(chi2, high=True)))
-            self.tblParams.setItem(i_row, 1, QtWidgets.QTableWidgetItem(str(filename)))
+            widget.setItem(i_row, 0, QtWidgets.QTableWidgetItem(GuiUtils.formatNumber(chi2, high=True)))
+            widget.setItem(i_row, 1, QtWidgets.QTableWidgetItem(str(filename)))
             # Now, all the parameters
             for i_col, param in enumerate(param_list[2:]):
@@ -238,12 +342,38 @@
                     # parameter is on the to-optimize list - get the optimized value
                     par_value = row[0].pvec[row[0].param_list.index(param)]
-                    # should we parse out errors here and store them?
+                    # parse out errors and store them for later use
+                    err_value = row[0].stderr[row[0].param_list.index(param)]
+                    if param in error_columns:
+                        error_columns[param].append(err_value)
+                    else:
+                        error_columns[param] = [i_col, err_value]
                 else:
                     # parameter was not varied
                     par_value = row[0].model.params[param]
-                self.tblParams.setItem(i_row, i_col+2, QtWidgets.QTableWidgetItem(
+
+                widget.setItem(i_row, i_col+2, QtWidgets.QTableWidgetItem(
                     GuiUtils.formatNumber(par_value, high=True)))
 
-        self.tblParams.resizeColumnsToContents()
+        # Add errors
+        error_list = list(error_columns.keys())
+        for error_param in error_list[::-1]: # must be reverse to keep column indices
+            # the offset for the err column: +2 from the first two extra columns, +1 to append this column
+            error_column = error_columns[error_param][0]+3
+            error_values = error_columns[error_param][1:]
+            widget.insertColumn(error_column)
+
+            column_name = error_param + self.ERROR_COLUMN_CAPTION
+            widget.setHorizontalHeaderItem(error_column, QtWidgets.QTableWidgetItem(column_name))
+
+            for i_row, error in enumerate(error_values):
+                item = QtWidgets.QTableWidgetItem(GuiUtils.formatNumber(error, high=True))
+                # Fancy, italic font for errors
+                font = QtGui.QFont()
+                font.setItalic(True)
+                item.setFont(font)
+                widget.setItem(i_row, error_column, item)
+
+        # resize content
+        widget.resizeColumnsToContents()
 
     @classmethod
Index: src/sas/qtgui/Utilities/GuiUtils.py
===================================================================
--- src/sas/qtgui/Utilities/GuiUtils.py	(revision 27689dcc2d7a762115970e3251a873f9f481f68c)
+++ src/sas/qtgui/Utilities/GuiUtils.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -245,4 +245,8 @@
     customModelDirectoryChanged = QtCore.pyqtSignal()
 
+    # Notify the gui manager about new data to be added to the grid view
+    sendDataToGridSignal = QtCore.pyqtSignal(list)
+
+
 def updateModelItemWithPlot(item, update_data, name=""):
     """
@@ -333,5 +337,4 @@
     """
     assert isinstance(item, QtGui.QStandardItem)
-    #assert isinstance(update_data, list)
 
     # Add the actual Data1D/Data2D object
@@ -465,4 +468,11 @@
 
     return info_item
+
+def dataFromItem(item):
+    """
+    Retrieve Data1D/2D component from QStandardItem.
+    The assumption - data stored in SasView standard, in child 0
+    """
+    return item.child(0).data()
 
 def openLink(url):
@@ -813,11 +823,4 @@
     return (xLabel, yLabel, xscale, yscale)
 
-def dataFromItem(item):
-    """
-    Retrieve Data1D/2D component from QStandardItem.
-    The assumption - data stored in SasView standard, in child 0
-    """
-    return item.child(0).data()
-
 def formatNumber(value, high=False):
     """
Index: src/sas/qtgui/Utilities/LocalConfig.py
===================================================================
--- src/sas/qtgui/Utilities/LocalConfig.py	(revision 3b3b40bf9d5056d902b19728c621dd2a4661d65f)
+++ src/sas/qtgui/Utilities/LocalConfig.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -134,5 +134,5 @@
 
 # Default threading model
-USING_TWISTED = False
+USING_TWISTED = True
 
 # Logging levels to disable, if any
Index: src/sas/qtgui/Utilities/TabbedModelEditor.py
===================================================================
--- src/sas/qtgui/Utilities/TabbedModelEditor.py	(revision 3b8cc00520d905f403798a463444af479d8bdd87)
+++ src/sas/qtgui/Utilities/TabbedModelEditor.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -35,4 +35,5 @@
         self.edit_only = edit_only
         self.is_modified = False
+        self.label = None
 
         self.addWidgets()
@@ -240,5 +241,6 @@
         # Run the model test in sasmodels
         try:
-            _ = GuiUtils.checkModel(full_path)
+            model_results = self.checkModel(full_path)
+            logging.info(model_results)
         except Exception as ex:
             msg = "Error building model: "+ str(ex)
Index: src/sas/qtgui/Utilities/UI/GridPanelUI.ui
===================================================================
--- src/sas/qtgui/Utilities/UI/GridPanelUI.ui	(revision 3b3b40bf9d5056d902b19728c621dd2a4661d65f)
+++ src/sas/qtgui/Utilities/UI/GridPanelUI.ui	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -8,5 +8,5 @@
     <y>0</y>
     <width>939</width>
-    <height>329</height>
+    <height>330</height>
    </rect>
   </property>
@@ -21,5 +21,35 @@
   </property>
   <widget class="QWidget" name="centralwidget">
-   <layout class="QGridLayout" name="gridLayout">
+   <layout class="QGridLayout" name="gridLayout_2">
+    <item row="0" column="0">
+     <widget class="QTabWidget" name="tabWidget">
+      <property name="tabPosition">
+       <enum>QTabWidget::South</enum>
+      </property>
+      <property name="currentIndex">
+       <number>0</number>
+      </property>
+      <widget class="QWidget" name="tab">
+       <attribute name="title">
+        <string>Tab 1</string>
+       </attribute>
+       <layout class="QGridLayout" name="gridLayout">
+        <item row="0" column="0">
+         <widget class="QTableWidget" name="tblParams">
+          <property name="contextMenuPolicy">
+           <enum>Qt::CustomContextMenu</enum>
+          </property>
+          <property name="alternatingRowColors">
+           <bool>true</bool>
+          </property>
+          <property name="selectionBehavior">
+           <enum>QAbstractItemView::SelectRows</enum>
+          </property>
+         </widget>
+        </item>
+       </layout>
+      </widget>
+     </widget>
+    </item>
     <item row="1" column="0">
      <layout class="QHBoxLayout" name="horizontalLayout">
@@ -36,4 +66,11 @@
         </property>
        </spacer>
+      </item>
+      <item>
+       <widget class="QPushButton" name="cmdPlot">
+        <property name="text">
+         <string>Plot</string>
+        </property>
+       </widget>
       </item>
       <item>
@@ -53,17 +90,4 @@
      </layout>
     </item>
-    <item row="0" column="0">
-     <widget class="QTableWidget" name="tblParams">
-      <property name="contextMenuPolicy">
-       <enum>Qt::CustomContextMenu</enum>
-      </property>
-      <property name="alternatingRowColors">
-       <bool>true</bool>
-      </property>
-      <property name="selectionBehavior">
-       <enum>QAbstractItemView::SelectRows</enum>
-      </property>
-     </widget>
-    </item>
    </layout>
   </widget>
@@ -73,5 +97,5 @@
      <x>0</x>
      <y>0</y>
-     <width>939</width>
+     <width>510</width>
      <height>26</height>
     </rect>
Index: src/sas/qtgui/Utilities/UnitTesting/GridPanelTest.py
===================================================================
--- src/sas/qtgui/Utilities/UnitTesting/GridPanelTest.py	(revision 3b3b40bf9d5056d902b19728c621dd2a4661d65f)
+++ src/sas/qtgui/Utilities/UnitTesting/GridPanelTest.py	(revision d4dac8028a51b020b8e01f0998d475e053206dea)
@@ -70,5 +70,5 @@
         self.assertIsInstance(self.widget, QtWidgets.QMainWindow)
         # Default title
-        self.assertEqual(self.widget.windowTitle(), "Grid Panel")
+        self.assertEqual(self.widget.windowTitle(), "Batch Fitting Results")
 
         # non-modal window
