Index: src/sas/sasgui/perspectives/calculator/image_viewer.py
===================================================================
--- src/sas/sasgui/perspectives/calculator/image_viewer.py	(revision a1b8fee73528559155793dd74cda07d0c32e9d72)
+++ src/sas/sasgui/perspectives/calculator/image_viewer.py	(revision 412e9e8b8f844bae61656ffb2a2d0c1c53e6e8de)
@@ -59,4 +59,10 @@
             _, extension = os.path.splitext(basename)
             try:
+                # Note that matplotlib only reads png natively.
+                # Any other formats (tiff, jpeg, etc) are passed
+                # to PIL which seems to have a problem in version
+                # 1.1.7 that causes a close error which shows up in 
+                # the log file.  This does not seem to have any adverse
+                # effects.  PDB   --- September 17, 2017.
                 img = mpimg.imread(file_path)
                 is_png = extension.lower() == '.png'
@@ -89,6 +95,16 @@
         if location is None:
             location = os.getcwd()
-        dlg = wx.FileDialog(self.parent, "Image Viewer: Choose a image file",
-                            location, "", "", style=wx.FD_OPEN | wx.FD_MULTIPLE)
+        wildcard="Images (*.bmp;*.gif;*jpeg,*jpg;*.png;*tif;*.tiff)|*bmp;\
+            *.gif; *.jpg; *.jpeg;*png;*.png;*.tif;*.tiff|"\
+            "Bitmap (*.bmp)|*.bmp|"\
+            "GIF (*.gif)|*.gif|"\
+            "JPEG (*.jpg;*.jpeg)|*.jpg;*.jpeg|"\
+            "PNG (*.png)|*.png|"\
+            "TIFF (*.tif;*.tiff)|*.tif;*tiff|"\
+            "All Files (*.*)|*.*|"
+
+        dlg = wx.FileDialog(self.parent, "Image Viewer: Choose an image file",
+                            location, "", wildcard, style=wx.FD_OPEN
+                            | wx.FD_MULTIPLE)
         if dlg.ShowModal() == wx.ID_OK:
             path = dlg.GetPaths()
Index: src/sas/sasgui/perspectives/calculator/media/image_viewer_help.rst
===================================================================
--- src/sas/sasgui/perspectives/calculator/media/image_viewer_help.rst	(revision da456fba6ab06ddec7ae847f352aff18d1d855d1)
+++ src/sas/sasgui/perspectives/calculator/media/image_viewer_help.rst	(revision 412e9e8b8f844bae61656ffb2a2d0c1c53e6e8de)
@@ -34,10 +34,10 @@
    will be displayed.
 
-.. image:: load_image.bmp
+.. image:: load_image.png
 
 3) To save, print, or copy the image, or to apply a grid overlay, right-click 
    anywhere in the plot.
 
-.. image:: pic_plot.bmp
+.. image:: pic_plot.png
 
 4. If the image is taken from a 2D detector, SasView can attempt to convert 
@@ -51,5 +51,5 @@
    then click the OK.
 
-.. image:: pic_convert.bmp
+.. image:: pic_convert.png
 
 .. ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ
Index: src/sas/sasgui/perspectives/fitting/basepage.py
===================================================================
--- src/sas/sasgui/perspectives/fitting/basepage.py	(revision a1b8fee73528559155793dd74cda07d0c32e9d72)
+++ src/sas/sasgui/perspectives/fitting/basepage.py	(revision 33dc18f1612814b89c60662f4f734c0d8831ac72)
@@ -1841,5 +1841,4 @@
             if models.name != "NoStructure":
                 mlist.append((models.name, models))
-
         # Sort the models
         mlist_sorted = sorted(mlist)
@@ -2929,4 +2928,39 @@
             return False
 
+
+    def _get_copy_params_details(self):
+        """
+        Combines polydisperse parameters with self.parameters so that they can
+        be written to the clipboard (for Excel or LaTeX). Also returns a list of
+        the names of parameters that have been fitted
+
+        :returns: all_params - A list of all parameters, in the format of 
+        self.parameters
+        :returns: fitted_par_names - A list of the names of parameters that have
+        been fitted
+        """
+        # Names of params that are being fitted
+        fitted_par_names = [param[1] for param in self.param_toFit]
+        # Names of params with associated polydispersity
+        disp_params = [param[1].split('.')[0] for param in self.fittable_param]
+
+        # Create array of all parameters
+        all_params = copy.copy(self.parameters)
+        for param in self.parameters:
+            if param[1] in disp_params:
+                # Polydisperse params aren't in self.parameters, so need adding
+                # to all_params
+                name = param[1] + ".width"
+                index = all_params.index(param) + 1
+                to_insert = []
+                if name in fitted_par_names:
+                    # Param is fitted, so already has a param list in self.param_toFit
+                    to_insert = self.param_toFit[fitted_par_names.index(name)]
+                else:
+                    # Param isn't fitted, so mockup a param list
+                    to_insert = [None, name, self.model.getParam(name), None, None]
+                all_params.insert(index, to_insert)
+        return all_params, fitted_par_names
+
     def get_copy_excel(self):
         """
@@ -2942,30 +2976,43 @@
         Get the string copies of the param names and values in the tap
         """
+        if not self.parameters:
+            # Do nothing if parameters doesn't exist
+            return False
+
         content = ''
-
         crlf = chr(13) + chr(10)
         tab = chr(9)
 
-        # Do it if params exist
-        if self.parameters:
-
-            for param in self.parameters:
-                content += param[1]  # parameter name
+        all_params, fitted_param_names = self._get_copy_params_details()
+
+        # Construct row of parameter names
+        for param in all_params:
+            name = param[1] # Parameter name
+            content += name
+            content += tab
+            if name in fitted_param_names:
+                # Only print errors for fitted parameters
+                content += name + "_err"
                 content += tab
-                content += param[1] + "_err"
-                content += tab
-
-            content += crlf
-
-            # row of values and errors...
-            for param in self.parameters:
-                content += param[2].GetValue()  # value
-                content += tab
-                content += param[4].GetValue()  # error
-                content += tab
-
-            return content
-        else:
-            return False
+
+        content += crlf
+
+        # Construct row of parameter values and errors
+        for param in all_params:
+            value = param[2]
+            if hasattr(value, 'GetValue'):
+                # param[2] is a text box
+                value = value.GetValue()
+            else:
+                # param[2] is a float (from our self._get_copy_params_details)
+                value = str(value)
+            content += value
+            content += tab
+            if param[1] in fitted_param_names:
+                # Only print errors for fitted parameters
+                content += param[4].GetValue()
+                content += tab 
+
+        return content
 
     def get_copy_latex(self):
@@ -2982,4 +3029,8 @@
         Get the string copies of the param names and values in the tap
         """
+        if not self.parameters:
+            # Do nothing if self.parameters doesn't exist
+            return False
+        
         content = '\\begin{table}'
         content += '\\begin{tabular}[h]'
@@ -2988,37 +3039,49 @@
         tab = chr(9)
 
-        # Do it if params exist
-        if self.parameters:
-
-            content += '{|'
-            for param in self.parameters:
-                content += 'l|l|'
-            content += '}\hline'
-            content += crlf
-
-            for index, param in enumerate(self.parameters):
-                content += param[1].replace('_', '\_')  # parameter name
+        all_params, fitted_param_names = self._get_copy_params_details()
+
+        content += '{|'
+        for param in all_params:
+            content += 'l|l|'
+        content += '}\hline'
+        content += crlf
+
+        # Construct row of parameter names
+        for index, param in enumerate(all_params):
+            name = param[1] # Parameter name
+            content += name.replace('_', '\_')  # Escape underscores
+            if name in fitted_param_names:
+                # Only print errors for fitted parameters
                 content += ' & '
-                content += param[1].replace('_', '\_') + "\_err"
-                if index < len(self.parameters) - 1:
-                    content += ' & '
-            content += '\\\\ \\hline'
-            content += crlf
-
-            # row of values and errors...
-            for index, param in enumerate(self.parameters):
-                content += param[2].GetValue()  # parameter value
+                content += name.replace('_', '\_') + "\_err"
+            if index < len(all_params) - 1:
                 content += ' & '
-                content += param[4].GetValue()  # parameter error
-                if index < len(self.parameters) - 1:
-                    content += ' & '
-            content += '\\\\ \\hline'
-            content += crlf
-
-            content += '\\end{tabular}'
-            content += '\\end{table}'
-            return content
-        else:
-            return False
+
+        content += '\\\\ \\hline'
+        content += crlf
+
+        # Construct row of values and errors
+        for index, param in enumerate(all_params):
+            value = param[2]
+            if hasattr(value, "GetValue"):
+                # value is a text box
+                value = value.GetValue()
+            else:
+                # value is a float (from self._get_copy_params_details)
+                value = str(value)
+            content += value
+            if param[1] in fitted_param_names:
+                # Only print errors for fitted params
+                content += ' & '
+                content += param[4].GetValue()
+            if index < len(all_params) - 1:
+                content += ' & '
+        
+        content += '\\\\ \\hline'
+        content += crlf
+        content += '\\end{tabular}'
+        content += '\\end{table}'
+
+        return content
 
     def set_clipboard(self, content=None):
Index: src/sas/sasgui/perspectives/fitting/fitpage.py
===================================================================
--- src/sas/sasgui/perspectives/fitting/fitpage.py	(revision 9a5097cd1c76689d1f997b85ba43d12a95d8f243)
+++ src/sas/sasgui/perspectives/fitting/fitpage.py	(revision 79e6a33e39a965552e3c863cc0b90d3d34b281c0)
@@ -289,5 +289,5 @@
         self.btFitHelp.SetToolTipString("General fitting help.")
         self.btFitHelp.Bind(wx.EVT_BUTTON, self._onFitHelp)
-        
+
         # Resolution Smearing Help button (for now use same technique as
         # used for dI help to get tiniest possible button that works
@@ -303,5 +303,5 @@
         self.btSmearHelp.SetToolTipString("Resolution smearing help.")
         self.btSmearHelp.Bind(wx.EVT_BUTTON, self._onSmearHelp)
-        
+
         # textcntrl for custom resolution
         self.smear_pinhole_percent = ModelTextCtrl(self, wx.ID_ANY,
@@ -564,5 +564,5 @@
         sizer.Add(self.draw_button, 0, 0)
         sizer.Add((-1, 5))
-        
+
         sizer.Add(self.tcChi, 0, 0)
         sizer.Add(self.Npts_fit, 0, 0)
@@ -570,5 +570,5 @@
         sizer.Add(self.btFit, 0, 0)
         sizer.Add(self.btFitHelp, 0, 0)
-        
+
         boxsizer_range.Add(sizer_chi2)
         boxsizer_range.Add(sizer)
@@ -1156,6 +1156,13 @@
                 copy_flag = self.get_copy_params()
                 is_poly_enabled = self.enable_disp.GetValue()
-
-        self._on_select_model_helper()
+        try:
+            self._on_select_model_helper()
+        except Exception as e:
+            evt = StatusEvent(status=e.message, info="error")
+            wx.PostEvent(self._manager.parent, evt)
+            # Set S(Q) to None
+            self.structurebox.SetSelection(0)
+            self._on_select_model()
+            return
         self.set_model_param_sizer(self.model)
         if self.model is None:
@@ -2181,4 +2188,7 @@
         self.save_current_state()
 
+        if not self.is_mac:
+            self.Layout()
+            self.Refresh()
         # plot model ( when drawing, do not update chisqr value again)
         self._draw_model(update_chisqr=False, source='fit')
@@ -2775,5 +2785,5 @@
             else:
                 return cmp(a.lower(), b.lower())
-        
+
         # keys obtained now from ordered dict, so commenting alphabetical
         # ordering keys.sort(custom_compare)
Index: src/sas/sasgui/perspectives/fitting/fitpanel.py
===================================================================
--- src/sas/sasgui/perspectives/fitting/fitpanel.py	(revision 67b0a99ce9f69a9dfef035e8e4a06588c9f1602f)
+++ src/sas/sasgui/perspectives/fitting/fitpanel.py	(revision 6f9abd39b6d696038bf298ef8e8b277439a2cb9c)
@@ -501,8 +501,13 @@
             if data is None:
                 return None
+        focused_page = self.GetPage(self.GetSelection())
         for page in self.opened_pages.values():
             # check if the selected data existing in the fitpanel
             pos = self.GetPageIndex(page)
             if not check_data_validity(page.get_data()) and not page.batch_on:
+                if page.model is not None and page != focused_page:
+                    # Page has an active theory and is in background - don't
+                    # send data here.
+                    continue
                 # make sure data get placed in 1D empty tab if data is 1D
                 # else data get place on 2D tab empty tab
Index: src/sas/sasgui/perspectives/fitting/fitting.py
===================================================================
--- src/sas/sasgui/perspectives/fitting/fitting.py	(revision 2d9526dfda3bdfe3c9159ab7d9db8399b4a00848)
+++ src/sas/sasgui/perspectives/fitting/fitting.py	(revision 2504b5a1018b4b2aff653769b6785e73300296bf)
@@ -357,4 +357,15 @@
                             else:
                                 page.formfactorbox.SetLabel(current_val)
+                        if hasattr(page, 'structurebox'):
+                            selected_name = page.structurebox.GetStringSelection()
+
+                            page.structurebox.Clear()
+                            page.initialize_combox()
+
+                            index = page.structurebox.FindString(selected_name)
+                            if index == -1:
+                                index = 0
+                            page.structurebox.SetSelection(index)
+                            page._on_select_model()
         except:
             logger.error("update_custom_combo: %s", sys.exc_value)
@@ -1326,5 +1337,5 @@
 
                 is_data2d = issubclass(data.__class__, Data2D)
-                #check consistency of arrays
+                # Check consistency of arrays
                 if not is_data2d:
                     if len(res.theory) == len(res.index[res.index]) and \
@@ -1337,8 +1348,14 @@
                     new_theory[res.index == False] = np.nan
                     correct_result = True
-                #get all fittable parameters of the current model
+                # Get all fittable parameters of the current model
                 param_list = model.getParamList()
                 for param in model.getDispParamList():
-                    if not model.is_fittable(param) and \
+                    if '.' in param and param in param_list:
+                        # Ensure polydispersity results are displayed
+                        p1, p2 = param.split('.')
+                        if not model.is_fittable(p1) and not (p2 == 'width' and param in res.param_list)\
+                            and param in param_list:
+                            param_list.remove(param)
+                    elif not model.is_fittable(param) and \
                         param in param_list:
                         param_list.remove(param)
@@ -1361,9 +1378,9 @@
                     batch_outputs["Chi2"].append(ERROR)
                     for param in param_list:
-                        # save value of  fixed parameters
+                        # Save value of  fixed parameters
                         if param not in res.param_list:
                             batch_outputs[str(param)].append(ERROR)
                         else:
-                            #save only fitted values
+                            # Save only fitted values
                             batch_outputs[param].append(ERROR)
                             batch_inputs["error on %s" % str(param)].append(ERROR)
Index: src/sas/sasgui/perspectives/fitting/models.py
===================================================================
--- src/sas/sasgui/perspectives/fitting/models.py	(revision b1c20119cf657ba734745bce8235e0d675629175)
+++ src/sas/sasgui/perspectives/fitting/models.py	(revision 632fda90d2ac7346a8a5e823a4e5ad1fff44b05f)
@@ -14,4 +14,5 @@
 import py_compile
 import shutil
+from copy import copy
 # Explicitly import from the pluginmodel module so that py2exe
 # places it in the distribution. The Model1DPlugin class is used
@@ -278,8 +279,8 @@
         """
 
-        # regular model names only
+        # Regular model names only
         self.model_name_list = []
 
-        #Build list automagically from sasmodels package
+        # Build list automagically from sasmodels package
         for model in load_standard_models():
             self.model_dictionary[model.name] = model
@@ -293,9 +294,19 @@
                 self.model_name_list.append(model.name)
 
-        #Looking for plugins
+        # Looking for plugins
         self.stored_plugins = self.findModels()
         self.plugins = self.stored_plugins.values()
         for name, plug in self.stored_plugins.iteritems():
             self.model_dictionary[name] = plug
+            # TODO: Remove 'hasattr' statements when old style plugin models
+            # are no longer supported. All sasmodels models will have
+            # the required attributes.
+            if hasattr(plug, 'is_structure_factor') and plug.is_structure_factor:
+                self.struct_list.append(plug)
+                self.plugins.remove(plug)
+            elif hasattr(plug, 'is_form_factor') and plug.is_form_factor:
+                self.multiplication_factor.append(plug)
+            if hasattr(plug, 'is_multiplicity_model') and plug.is_multiplicity_model:
+                self.multi_func_list.append(plug)
 
         self._get_multifunc_models()
@@ -340,16 +351,48 @@
         """
         self.plugins = []
-        new_plugins = _find_models()
-        for name, plug in  new_plugins.iteritems():
-            for stored_name, stored_plug in self.stored_plugins.iteritems():
-                if name == stored_name:
-                    del self.stored_plugins[name]
-                    del self.model_dictionary[name]
-                    break
+        self.stored_plugins = _find_models()
+        structure_names = [model.name for model in self.struct_list]
+        form_names = [model.name for model in self.multiplication_factor]
+
+        # Remove all plugin structure factors and form factors
+        for name in copy(structure_names):
+            if '[plug-in]' in name:
+                i = structure_names.index(name)
+                del self.struct_list[i]
+                structure_names.remove(name)
+        for name in copy(form_names):
+            if '[plug-in]' in name:
+                i = form_names.index(name)
+                del self.multiplication_factor[i]
+                form_names.remove(name)
+
+        # Add new plugin structure factors and form factors
+        for name, plug in self.stored_plugins.iteritems():
+            if plug.is_structure_factor:
+                if name in structure_names:
+                    # Delete the old model from self.struct list
+                    i = structure_names.index(name)
+                    del self.struct_list[i]
+                # Add the new model to self.struct_list
+                self.struct_list.append(plug)
+            elif plug.is_form_factor:
+                if name in form_names:
+                    # Delete the old model from self.multiplication_factor
+                    i = form_names.index(name)
+                    del self.multiplication_factor[i]
+                # Add the new model to self.multiplication_factor
+                self.multiplication_factor.append(plug)
+
+            # Add references to the updated model
             self.stored_plugins[name] = plug
-            self.plugins.append(plug)
+            if not plug.is_structure_factor:
+                # Don't show S(Q) models in the 'Plugin Models' dropdown
+                self.plugins.append(plug)
             self.model_dictionary[name] = plug
 
         self.model_combobox.reset_list("Plugin Models", self.plugins)
+        self.model_combobox.reset_list("Structure Factors", self.struct_list)
+        self.model_combobox.reset_list("P(Q)*S(Q)", self.multiplication_factor)
+
         return self.model_combobox.get_list()
 
