Index: src/sas/qtgui/Utilities/GuiUtils.py
===================================================================
--- src/sas/qtgui/Utilities/GuiUtils.py	(revision 67346f9ffd11c962adee23ee46fee9d0d9c5a62a)
+++ src/sas/qtgui/Utilities/GuiUtils.py	(revision 133812c7ffae92cd47338d64c4d757261f78a6a6)
@@ -11,4 +11,6 @@
 import webbrowser
 import urllib.parse
+import json
+from io import BytesIO
 
 import numpy as np
@@ -26,4 +28,13 @@
 from sas.qtgui.Plotting.PlotterData import Data1D
 from sas.qtgui.Plotting.PlotterData import Data2D
+from sas.qtgui.Plotting.Plottables import Plottable
+from sas.sascalc.dataloader.data_info import Sample, Source, Vector
+from sas.qtgui.Plotting.Plottables import View
+from sas.qtgui.Plotting.Plottables import PlottableTheory1D
+from sas.qtgui.Plotting.Plottables import PlottableFit1D
+from sas.qtgui.Plotting.Plottables import Text
+from sas.qtgui.Plotting.Plottables import Chisq
+from sas.qtgui.MainWindow.DataState import DataState
+
 from sas.sascalc.dataloader.loader import Loader
 from sas.qtgui.Utilities import CustomDir
@@ -259,7 +270,4 @@
     sendDataToGridSignal = QtCore.pyqtSignal(list)
 
-    # Action Save Analysis triggered
-    saveAnalysisSignal = QtCore.pyqtSignal()
-
     # Mask Editor requested
     maskEditorSignal = QtCore.pyqtSignal(Data2D)
@@ -292,5 +300,5 @@
     forcePlotDisplaySignal = QtCore.pyqtSignal(list)
 
-def updateModelItemWithPlot(item, update_data, name=""):
+def updateModelItemWithPlot(item, update_data, name="", checkbox_state=None):
     """
     Adds a checkboxed row named "name" to QStandardItem
@@ -317,8 +325,9 @@
             # Force redisplay
             return
-
     # Create the new item
     checkbox_item = createModelItemWithPlot(update_data, name)
 
+    if checkbox_state is not None:
+        checkbox_item.setCheckState(checkbox_state)
     # Append the new row to the main item
     item.appendRow(checkbox_item)
@@ -571,4 +580,6 @@
     if isinstance(data.process, list) and data.process:
         for process in data.process:
+            if process is None:
+                continue
             process_date = process.date
             process_date_item = QtGui.QStandardItem("Date: " + process_date)
@@ -1145,4 +1156,266 @@
     return result
 
+def saveData(fp, data):
+    """
+    save content of data to fp (a .write()-supporting file-like object)
+    """
+
+    def add_type(dict, type):
+        dict['__type__'] = type.__name__
+        return dict
+
+    def jdefault(o):
+        """
+        objects that can't otherwise be serialized need to be converted
+        """
+        # tuples and sets (TODO: default JSONEncoder converts tuples to lists, create custom Encoder that preserves tuples)
+        if isinstance(o, (tuple, set)):
+            content = { 'data': list(o) }
+            return add_type(content, type(o))
+
+        # "simple" types
+        if isinstance(o, (Sample, Source, Vector)):
+            return add_type(o.__dict__, type(o))
+        if isinstance(o, (Plottable, View)):
+            return add_type(o.__dict__, type(o))
+
+        # DataState
+        if isinstance(o, (Data1D, Data2D)):
+            # don't store parent
+            content = o.__dict__.copy()
+            #content.pop('parent')
+            return add_type(content, type(o))
+
+        # ndarray
+        if isinstance(o, np.ndarray):
+            buffer = BytesIO()
+            np.save(buffer, o)
+            buffer.seek(0)
+            content = { 'data': buffer.read().decode('latin-1') }
+            return add_type(content, type(o))
+
+        # not supported
+        logging.info("data cannot be serialized to json: %s" % type(o))
+        return None
+
+    json.dump(data, fp, indent=2, sort_keys=True, default=jdefault)
+
+def readDataFromFile(fp):
+    '''
+    Reads in Data1D/Data2 datasets from the file.
+    Datasets are stored in the JSON format.
+    '''
+    supported = [
+        tuple, set,
+        Sample, Source, Vector,
+        Plottable, Data1D, Data2D, PlottableTheory1D, PlottableFit1D, Text, Chisq, View,
+        DataState, np.ndarray]
+
+    lookup = dict((cls.__name__, cls) for cls in supported)
+
+    class TooComplexException(Exception):
+        pass
+
+    def simple_type(cls, data, level):
+        class Empty(object):
+            def __init__(self):
+                for key, value in data.items():
+                    setattr(self, key, generate(value, level))
+
+        # create target object
+        o = Empty()
+        o.__class__ = cls
+
+        return o
+
+    def construct(type, data, level):
+        try:
+            cls = lookup[type]
+        except KeyError:
+            logging.info('unknown type: %s' % type)
+            return None
+
+        # tuples and sets
+        if cls in (tuple, set):
+            # convert list to tuple/set
+            return cls(generate(data['data'], level))
+
+        # "simple" types
+        if cls in (Sample, Source, Vector):
+            return simple_type(cls, data, level)
+        if issubclass(cls, Plottable) or (cls == View):
+            return simple_type(cls, data, level)
+
+        # DataState
+        if cls == DataState:
+            o = simple_type(cls, data, level)
+            o.parent = None # TODO: set to ???
+            return o
+
+        # ndarray
+        if cls == np.ndarray:
+            buffer = BytesIO()
+            buffer.write(data['data'].encode('latin-1'))
+            buffer.seek(0)
+            return np.load(buffer)
+
+        logging.info('not implemented: %s, %s' % (type, cls))
+        return None
+
+    def generate(data, level):
+        if level > 16: # recursion limit (arbitrary number)
+            raise TooComplexException()
+        else:
+            level += 1
+
+        if isinstance(data, dict):
+            try:
+                type = data['__type__']
+            except KeyError:
+                # if dictionary doesn't have __type__ then it is assumed to be just an ordinary dictionary
+                o = {}
+                for key, value in data.items():
+                    o[key] = generate(value, level)
+                return o
+
+            return construct(type, data, level)
+
+        if isinstance(data, list):
+            return [generate(item, level) for item in data]
+
+        return data
+
+    new_stored_data = {}
+    for id, data in json.load(fp).items():
+        try:
+            new_stored_data[id] = generate(data, 0)
+        except TooComplexException:
+            logging.info('unable to load %s' % id)
+
+    return new_stored_data
+
+def readProjectFromSVS(filepath):
+    """
+    Read old SVS file and convert to the project dictionary
+    """
+    from sas.sascalc.dataloader.readers.cansas_reader import Reader as CansasReader
+    from sas.sascalc.fit.pagestate import Reader
+
+    loader = Loader()
+    loader.associate_file_reader('.svs', Reader)
+    temp = loader.load(filepath)
+    state_reader = Reader()
+    data_svs, state_svs = state_reader.read(filepath)
+
+    output = []
+    if isinstance(temp, list) and isinstance(state_svs, list):
+        for item, state in zip(temp, state_svs):
+            output.append([item, state])
+    else:
+        output[temp, state_svs]
+    return output
+
+def convertFromSVS(datasets):
+    """
+    Read in properties from SVS and convert into a simple dict
+    """
+    content = {}
+    for dataset in datasets:
+        # we already have data - interested only in properties
+        #[[item_1, state_1], [item_2, state_2],...]
+        data = dataset[0]
+        params = dataset[1]
+        content[params.data_id] = {}
+        content[params.data_id]['fit_data'] = [data, {'checked': 2}, []]
+        param_dict = {}
+        param_dict['fitpage_category'] = [params.categorycombobox]
+        param_dict['fitpage_model'] = [params.formfactorcombobox]
+        param_dict['fitpage_structure'] = [params.structurecombobox]
+        param_dict['2D_params'] = [str(params.is_2D)]
+        param_dict['chainfit_params'] = ["False"]
+        param_dict['data_id'] = [params.data_id]
+        param_dict['data_name'] = [params.data_name]
+        param_dict['is_data'] = [str(params.is_data)]
+        param_dict['magnetic_params'] = [str(params.magnetic_on)]
+        param_dict['model_name'] = [params.formfactorcombobox]
+        param_dict['polydisperse_params'] = [str(params.enable_disp)]
+        param_dict['q_range_max'] = [str(params.qmax)]
+        param_dict['q_range_min'] = [str(params.qmin)]
+        # Smearing is a bit trickier. 4.x has multiple keywords,
+        # one for each combobox option
+        if params.enable_smearer:
+            if params.slit_smearer:
+                w = 1
+            elif params.pinhole_smearer:
+                w = 2
+            else:
+                w = 0
+            param_dict['smearing'] = [str(w)]
+        # weighting is also tricky. 4.x has multiple keywords,
+        # one for each radio box.
+        if params.dI_noweight:
+            w = 2
+        elif params.dI_didata:
+            w = 3
+        elif params.dI_sqrdata:
+            w = 4
+        elif params.dI_idata:
+            w = 5
+        else:
+            w = 2
+        param_dict['weighting'] = [str(w)]
+
+        # 4.x multi_factor is really the multiplicity
+        if params.multi_factor is not None:
+            param_dict['multiplicity'] = [str(int(params.multi_factor))]
+
+        # playing with titles
+        data.filename = params.file
+        data.title = params.data_name
+        data.name = params.data_name
+
+        # main parameters
+        for p in params.parameters:
+            p_name = p[1]
+            param_dict[p_name] = [str(p[0]), str(p[2]), None, str(p[5][1]), str(p[6][1])]
+        # orientation parameters
+        if params.is_2D:
+            for p in params.orientation_params:
+                p_name = p[1]
+                p_min = "-360.0"
+                p_max = "360.0"
+                if p[5][1] != "":
+                    p_min = p[5][1]
+                if p[6][1] != "":
+                    p_max = p[6][1]
+                param_dict[p_name] = [str(p[0]), str(p[2]), None, p_min, p_max]
+
+        # disperse parameters
+        if params.enable_disp:
+            for p in params.fittable_param:
+                p_name = p[1]
+                p_opt = str(p[0])
+                p_err = "0"
+                p_width = str(p[2])
+                p_min = str(0)
+                p_max = "inf"
+                param_npts = p_name.replace('.width','.npts')
+                param_nsigmas = p_name.replace('.width', '.nsigmas')
+                if params.is_2D and p_name in params.disp_obj_dict:
+                    lookup = params.orientation_params_disp
+                    p_min = "-360.0"
+                    p_max = "360.0"
+                else:
+                    lookup = params.fixed_param
+                p_npts = [s[2] for s in lookup if s[1] == param_npts][0]
+                p_nsigmas = [s[2] for s in lookup if s[1] == param_nsigmas][0]
+                if p_name in params.disp_obj_dict:
+                    p_disp = params.disp_obj_dict[p_name]
+                else:
+                    p_disp = "gaussian"
+                param_dict[p_name] = [p_opt, p_width, p_min, p_max, p_npts, p_nsigmas, p_disp]
+
+        content[params.data_id]['fit_params'] = param_dict
+    return content
 
 def enum(*sequential, **named):
