Index: src/sas/qtgui/DataExplorer.py
===================================================================
--- src/sas/qtgui/DataExplorer.py	(revision f82ab8cebb48e29a3c2598340d12929e037d764c)
+++ src/sas/qtgui/DataExplorer.py	(revision 481ff261d4487dbfcc8eabb342125294e0c6f514)
@@ -7,4 +7,6 @@
 from PyQt4 import QtGui
 from PyQt4 import QtWebKit
+from PyQt4.Qt import QMutex
+
 from twisted.internet import threads
 
@@ -38,4 +40,7 @@
         self.loader = Loader()
         self.manager = DataManager()
+
+        # Be careful with twisted threads.
+        self.mutex = QMutex()
 
         # Connect the buttons
@@ -46,4 +51,9 @@
         self.cmdSendTo.clicked.connect(self.sendData)
         self.cmdNew.clicked.connect(self.newPlot)
+        self.cmdHelp.clicked.connect(self.displayHelp)
+        self.cmdHelp_2.clicked.connect(self.displayHelp)
+
+        # Display HTML content
+        self._helpView = QtWebKit.QWebView()
 
         # Connect the comboboxes
@@ -56,12 +66,22 @@
 
         # Proxy model for showing a subset of Data1D/Data2D content
-        self.proxy = QtGui.QSortFilterProxyModel(self)
-        self.proxy.setSourceModel(self.model)
+        self.data_proxy = QtGui.QSortFilterProxyModel(self)
+        self.data_proxy.setSourceModel(self.model)
+
+        # Don't show "empty" rows with data objects
+        self.data_proxy.setFilterRegExp(r"[^()]")
 
         # The Data viewer is QTreeView showing the proxy model
-        self.treeView.setModel(self.proxy)
+        self.treeView.setModel(self.data_proxy)
+
+        # Proxy model for showing a subset of Theory content
+        self.theory_proxy = QtGui.QSortFilterProxyModel(self)
+        self.theory_proxy.setSourceModel(self.theory_model)
+
+        # Don't show "empty" rows with data objects
+        self.theory_proxy.setFilterRegExp(r"[^()]")
 
         # Theory model view
-        #self.freezeView.setModel(self.theory_model)
+        self.freezeView.setModel(self.theory_proxy)
 
     def closeEvent(self, event):
@@ -70,4 +90,12 @@
         """
         event.ignore()
+
+    def displayHelp(self):
+        """
+        Show the "Loading data" section of help
+        """
+        _TreeLocation = "html/user/sasgui/guiframe/data_explorer_help.html"
+        self._helpView.load(QtCore.QUrl(_TreeLocation))
+        self._helpView.show()
 
     def loadFromURL(self, url):
@@ -81,5 +109,5 @@
         """
         Called when the "Load" button pressed.
-        Opens the Qt "Open File..." dialog 
+        Opens the Qt "Open File..." dialog
         """
         path_str = self.chooseFiles()
@@ -91,18 +119,19 @@
         """
         Called when the "File/Load Folder" menu item chosen.
-        Opens the Qt "Open Folder..." dialog 
-        """
-        dir = QtGui.QFileDialog.getExistingDirectory(self, "Choose a directory", "",
+        Opens the Qt "Open Folder..." dialog
+        """
+        folder = QtGui.QFileDialog.getExistingDirectory(self, "Choose a directory", "",
               QtGui.QFileDialog.ShowDirsOnly | QtGui.QFileDialog.DontUseNativeDialog)
-        if dir is None:
-            return
-
-        dir = str(dir)
-
-        if not os.path.isdir(dir):
+        if folder is None:
+            return
+
+        folder = str(folder)
+
+        if not os.path.isdir(folder):
             return
 
         # get content of dir into a list
-        path_str = [os.path.join(os.path.abspath(dir), filename) for filename in os.listdir(dir)]
+        path_str = [os.path.join(os.path.abspath(folder), filename)
+                        for filename in os.listdir(folder)]
 
         self.loadFromURL(path_str)
@@ -211,8 +240,8 @@
         and copying it to a separate top-level item.
         """
-        import copy
         # Figure out which _inner_ rows are checked
         # Use 'while' so the row count is forced at every iteration
         outer_index = -1
+        theories_copied = 0
         while outer_index < self.model.rowCount():
             outer_index += 1
@@ -220,18 +249,36 @@
             if not outer_item:
                 continue
-            for inner_index in xrange(outer_item.rowCount()):
+            for inner_index in xrange(outer_item.rowCount()): # Should be just two rows: data and Info
                 subitem = outer_item.child(inner_index)
                 if subitem and subitem.isCheckable() and subitem.checkState() == QtCore.Qt.Checked:
-                    # Update the main model
-                    new_item = subitem.takeRow(inner_index)
-                    #new_item = QtGui.QStandardItem(subitem)
-                    #new_item = subitem.clone()
-                    #new_item = QtGui.QStandardItem()
-                    #new_item = copy.deepcopy(subitem)
-                    #super(new_item, self).__init__()   
-                    self.model.insertRow(0, new_item)
-                    outer_index += 1
-
-        self.model.reset()
+                    theories_copied += 1
+                    new_item = self.recursivelyCloneItem(subitem)
+                    self.theory_model.appendRow(new_item)
+            self.theory_model.reset()
+
+        freeze_msg = ""
+        if theories_copied == 0:
+            return
+        elif theories_copied == 1:
+            freeze_msg = "1 theory sent to Theory tab"
+        elif theories_copied > 1:
+            freeze_msg = "%i theories sent to Theory tab" % theories_copied
+        else:
+            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
+            raise AttributeError, freeze_msg
+        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
+        # Actively switch tabs
+        self.setCurrentIndex(1)
+
+    def recursivelyCloneItem(self, item):
+        """
+        Clone QStandardItem() object
+        """
+        new_item = item.clone()
+        # clone doesn't do deepcopy :(
+        for child_index in xrange(item.rowCount()):
+            child_item = self.recursivelyCloneItem(item.child(child_index))
+            new_item.setChild(child_index, child_item)
+        return new_item
 
     def newPlot(self):
@@ -266,5 +313,6 @@
             return
 
-        if type(paths) == QtCore.QStringList:
+        #if type(paths) == QtCore.QStringList:
+        if isinstance(paths, QtCore.QStringList):
             paths = [str(f) for f in paths]
 
@@ -276,6 +324,6 @@
     def readData(self, path):
         """
-        verbatim copy/paste from
-            sasgui\guiframe\local_perspectives\data_loader\data_loader.py
+        verbatim copy-paste from
+           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
         slightly modified for clarity
         """
@@ -286,7 +334,6 @@
         data_error = False
         error_message = ""
-        
+
         for p_file in path:
-            info = "info"
             basename = os.path.basename(p_file)
             _, extension = os.path.splitext(basename)
@@ -315,12 +362,16 @@
 
                 for item in output_objects:
-                    # cast sascalc.dataloader.data_info.Data1D into sasgui.guiframe.dataFitting.Data1D
+                    # cast sascalc.dataloader.data_info.Data1D into
+                    # sasgui.guiframe.dataFitting.Data1D
                     # TODO : Fix it
                     new_data = self.manager.create_gui_data(item, p_file)
                     output[new_data.id] = new_data
+
+                    # Model update should be protected
+                    self.mutex.lock()
                     self.updateModel(new_data, p_file)
                     self.model.reset()
-
                     QtGui.qApp.processEvents()
+                    self.mutex.unlock()
 
                     if hasattr(item, 'errors'):
@@ -354,7 +405,6 @@
                     error_message += "%s\n" % str(p_file)
                 info = "error"
-        
+
         if any_error or error_message:
-            # self.loadUpdate(output=output, message=error_message, info=info)
             self.communicator.statusBarUpdateSignal.emit(error_message)
 
@@ -362,4 +412,5 @@
             message = "Loading Data Complete! "
         message += log_msg
+
         return output, message
 
@@ -470,10 +521,7 @@
         Post message to status bar and update the data manager
         """
-        # Don't show "empty" rows with data objects
-        self.proxy.setFilterRegExp(r"[^()]")
-
         # Reset the model so the view gets updated.
         self.model.reset()
-        assert type(output)== tuple
+        assert type(output) == tuple
 
         output_data = output[0]
@@ -486,9 +534,10 @@
     def updateModel(self, data, p_file):
         """
+        Add data and Info fields to the model item
         """
         # Structure of the model
         # checkbox + basename
+        #     |-------> Data.D object
         #     |-------> Info
-        #                 |----> Data.D object
         #                 |----> Title:
         #                 |----> Run:
@@ -519,5 +568,5 @@
         # New row in the model
         self.model.appendRow(checkbox_item)
-        
+
     def updateModelFromPerspective(self, model_item):
         """
@@ -537,5 +586,5 @@
         # Pass acting as a debugger anchor
         pass
-       
+
 
 if __name__ == "__main__":
