source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ ebcdb02

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since ebcdb02 was ebcdb02, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Analysis only for Fitting, project save on other perspectives saves
datasets. Fixed tab deletion on data removal for batch tabs.

  • Property mode set to 100644
File size: 65.3 KB
Line 
1# global
2import sys
3import os
4import time
5import logging
6
7from PyQt5 import QtCore
8from PyQt5 import QtGui
9from PyQt5 import QtWidgets
10
11from twisted.internet import threads
12
13# SASCALC
14from sas.sascalc.dataloader.loader import Loader
15
16# QTGUI
17import sas.qtgui.Utilities.GuiUtils as GuiUtils
18import sas.qtgui.Plotting.PlotHelper as PlotHelper
19
20from sas.qtgui.Plotting.PlotterData import Data1D
21from sas.qtgui.Plotting.PlotterData import Data2D
22from sas.qtgui.Plotting.Plotter import Plotter
23from sas.qtgui.Plotting.Plotter2D import Plotter2D
24from sas.qtgui.Plotting.MaskEditor import MaskEditor
25
26from sas.qtgui.MainWindow.DataManager import DataManager
27from sas.qtgui.MainWindow.DroppableDataLoadWidget import DroppableDataLoadWidget
28
29import sas.qtgui.Perspectives as Perspectives
30
31DEFAULT_PERSPECTIVE = "Fitting"
32
33logger = logging.getLogger(__name__)
34
35class DataExplorerWindow(DroppableDataLoadWidget):
36    # The controller which is responsible for managing signal slots connections
37    # for the gui and providing an interface to the data model.
38
39    def __init__(self, parent=None, guimanager=None, manager=None):
40        super(DataExplorerWindow, self).__init__(parent, guimanager)
41
42        # Main model for keeping loaded data
43        self.model = QtGui.QStandardItemModel(self)
44        # Secondary model for keeping frozen data sets
45        self.theory_model = QtGui.QStandardItemModel(self)
46
47        # GuiManager is the actual parent, but we needed to also pass the QMainWindow
48        # in order to set the widget parentage properly.
49        self.parent = guimanager
50        self.loader = Loader()
51        self.manager = manager if manager is not None else DataManager()
52        self.txt_widget = QtWidgets.QTextEdit(None)
53
54        # Be careful with twisted threads.
55        self.mutex = QtCore.QMutex()
56
57        # Plot widgets {name:widget}, required to keep track of plots shown as MDI subwindows
58        self.plot_widgets = {}
59
60        # Active plots {id:Plotter1D/2D}, required to keep track of currently displayed plots
61        self.active_plots = {}
62
63        # Connect the buttons
64        self.cmdLoad.clicked.connect(self.loadFile)
65        self.cmdDeleteData.clicked.connect(self.deleteFile)
66        self.cmdDeleteTheory.clicked.connect(self.deleteTheory)
67        self.cmdFreeze.clicked.connect(self.freezeTheory)
68        self.cmdSendTo.clicked.connect(self.sendData)
69        self.cmdNew.clicked.connect(self.newPlot)
70        self.cmdNew_2.clicked.connect(self.newPlot)
71        self.cmdAppend.clicked.connect(self.appendPlot)
72        self.cmdAppend_2.clicked.connect(self.appendPlot)
73        self.cmdHelp.clicked.connect(self.displayHelp)
74        self.cmdHelp_2.clicked.connect(self.displayHelp)
75
76        # Fill in the perspectives combo
77        self.initPerspectives()
78
79        # Custom context menu
80        self.treeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
81        self.treeView.customContextMenuRequested.connect(self.onCustomContextMenu)
82        self.contextMenu()
83
84        # Same menus for the theory view
85        self.freezeView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
86        self.freezeView.customContextMenuRequested.connect(self.onCustomContextMenu)
87
88        # Connect the comboboxes
89        self.cbSelect.activated.connect(self.selectData)
90
91        #self.closeEvent.connect(self.closeEvent)
92        self.currentChanged.connect(self.onTabSwitch)
93        self.communicator = self.parent.communicator()
94        self.communicator.fileReadSignal.connect(self.loadFromURL)
95        self.communicator.activeGraphsSignal.connect(self.updateGraphCount)
96        self.communicator.activeGraphName.connect(self.updatePlotName)
97        self.communicator.plotUpdateSignal.connect(self.updatePlot)
98        self.communicator.maskEditorSignal.connect(self.showEditDataMask)
99        self.communicator.extMaskEditorSignal.connect(self.extShowEditDataMask)
100        self.communicator.changeDataExplorerTabSignal.connect(self.changeTabs)
101
102        self.cbgraph.editTextChanged.connect(self.enableGraphCombo)
103        self.cbgraph.currentIndexChanged.connect(self.enableGraphCombo)
104
105        # Proxy model for showing a subset of Data1D/Data2D content
106        self.data_proxy = QtCore.QSortFilterProxyModel(self)
107        self.data_proxy.setSourceModel(self.model)
108
109        # Don't show "empty" rows with data objects
110        self.data_proxy.setFilterRegExp(r"[^()]")
111
112        # The Data viewer is QTreeView showing the proxy model
113        self.treeView.setModel(self.data_proxy)
114
115        # Proxy model for showing a subset of Theory content
116        self.theory_proxy = QtCore.QSortFilterProxyModel(self)
117        self.theory_proxy.setSourceModel(self.theory_model)
118
119        # Don't show "empty" rows with data objects
120        self.theory_proxy.setFilterRegExp(r"[^()]")
121
122        # Theory model view
123        self.freezeView.setModel(self.theory_proxy)
124
125        self.enableGraphCombo(None)
126
127        # Current view on model
128        self.current_view = self.treeView
129
130    def closeEvent(self, event):
131        """
132        Overwrite the close event - no close!
133        """
134        event.ignore()
135
136    def onTabSwitch(self, index):
137        """ Callback for tab switching signal """
138        if index == 0:
139            self.current_view = self.treeView
140        else:
141            self.current_view = self.freezeView
142
143    def changeTabs(self, tab=0):
144        """
145        Switch tabs of the data explorer
146        0: data tab
147        1: theory tab
148        """
149        assert(tab in [0,1])
150        self.setCurrentIndex(tab)
151
152    def displayHelp(self):
153        """
154        Show the "Loading data" section of help
155        """
156        tree_location = "/user/qtgui/MainWindow/data_explorer_help.html"
157        self.parent.showHelp(tree_location)
158
159    def enableGraphCombo(self, combo_text):
160        """
161        Enables/disables "Assign Plot" elements
162        """
163        self.cbgraph.setEnabled(len(PlotHelper.currentPlots()) > 0)
164        self.cmdAppend.setEnabled(len(PlotHelper.currentPlots()) > 0)
165
166    def initPerspectives(self):
167        """
168        Populate the Perspective combobox and define callbacks
169        """
170        available_perspectives = sorted([p for p in list(Perspectives.PERSPECTIVES.keys())])
171        if available_perspectives:
172            self.cbFitting.clear()
173            self.cbFitting.addItems(available_perspectives)
174        self.cbFitting.currentIndexChanged.connect(self.updatePerspectiveCombo)
175        # Set the index so we see the default (Fitting)
176        self.cbFitting.setCurrentIndex(self.cbFitting.findText(DEFAULT_PERSPECTIVE))
177
178    def _perspective(self):
179        """
180        Returns the current perspective
181        """
182        return self.parent.perspective()
183
184    def loadFromURL(self, url):
185        """
186        Threaded file load
187        """
188        load_thread = threads.deferToThread(self.readData, url)
189        load_thread.addCallback(self.loadComplete)
190        load_thread.addErrback(self.loadFailed)
191
192    def loadFile(self, event=None):
193        """
194        Called when the "Load" button pressed.
195        Opens the Qt "Open File..." dialog
196        """
197        path_str = self.chooseFiles()
198        if not path_str:
199            return
200        self.loadFromURL(path_str)
201
202    def loadFolder(self, event=None):
203        """
204        Called when the "File/Load Folder" menu item chosen.
205        Opens the Qt "Open Folder..." dialog
206        """
207        folder = QtWidgets.QFileDialog.getExistingDirectory(self, "Choose a directory", "",
208              QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
209        if folder is None:
210            return
211
212        folder = str(folder)
213
214        if not os.path.isdir(folder):
215            return
216
217        # get content of dir into a list
218        path_str = [os.path.join(os.path.abspath(folder), filename)
219                    for filename in os.listdir(folder)]
220
221        self.loadFromURL(path_str)
222
223    def loadProject(self):
224        """
225        Called when the "Open Project" menu item chosen.
226        """
227        # check if any items loaded and warn about data deletion
228        if self.model.rowCount() > 0:
229            msg = "This operation will set remove all data, plots and analyses from"
230            msg += " SasView before loading the project. Do you wish to continue?"
231            msgbox = QtWidgets.QMessageBox(self)
232            msgbox.setIcon(QtWidgets.QMessageBox.Warning)
233            msgbox.setText(msg)
234            msgbox.setWindowTitle("Project Load")
235            # custom buttons
236            button_yes = QtWidgets.QPushButton("Yes")
237            msgbox.addButton(button_yes, QtWidgets.QMessageBox.YesRole)
238            button_no = QtWidgets.QPushButton("No")
239            msgbox.addButton(button_no, QtWidgets.QMessageBox.RejectRole)
240            retval = msgbox.exec_()
241            if retval == QtWidgets.QMessageBox.RejectRole:
242                # cancel fit
243                return
244
245        kwargs = {
246            'parent'    : self,
247            'caption'   : 'Open Project',
248            'filter'    : 'Project Files (*.json);;Old Project Files (*.svs);;All files (*.*)',
249            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
250        }
251        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
252        if filename:
253            self.deleteAllItems()
254            self.readProject(filename)
255
256    def loadAnalysis(self):
257        """
258        Called when the "Open Analysis" menu item chosen.
259        """
260        kwargs = {
261            'parent'    : self,
262            'caption'   : 'Open Analysis',
263            'filter'    : 'Project (*.fitv);;All files (*.*)',
264            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
265        }
266        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
267        if filename:
268            self.readProject(filename)
269
270    def saveProject(self):
271        """
272        Called when the "Save Project" menu item chosen.
273        """
274        kwargs = {
275            'parent'    : self,
276            'caption'   : 'Save Project',
277            'filter'    : 'Project (*.json)',
278            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
279        }
280        name_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
281        filename = name_tuple[0]
282        if not filename:
283            return
284        _, extension = os.path.splitext(filename)
285        if not extension:
286            filename = '.'.join((filename, 'json'))
287        self.communicator.statusBarUpdateSignal.emit("Saving Project... %s\n" % os.path.basename(filename))
288
289        return filename
290
291    def saveAsAnalysisFile(self, tab_id=1):
292        """
293        Show the save as... dialog and return the chosen filepath
294        """
295        default_name = "FitPage"+str(tab_id)+".fitv"
296
297        wildcard = "fitv files (*.fitv)"
298        kwargs = {
299            'caption'   : 'Save As',
300            'directory' : default_name,
301            'filter'    : wildcard,
302            'parent'    : None,
303        }
304        # Query user for filename.
305        filename_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
306        filename = filename_tuple[0]
307        return filename
308
309    def saveAnalysis(self, data, tab_id=1):
310        """
311        Called when the "Save Analysis" menu item chosen.
312        """
313        filename = self.saveAsAnalysisFile(tab_id)
314        if not filename:
315            return
316        _, extension = os.path.splitext(filename)
317        if not extension:
318            filename = '.'.join((filename, 'fitv'))
319        self.communicator.statusBarUpdateSignal.emit("Saving analysis... %s\n" % os.path.basename(filename))
320
321        with open(filename, 'w') as outfile:
322            GuiUtils.saveData(outfile, data)
323
324        self.communicator.statusBarUpdateSignal.emit('Analysis saved.')
325
326    def allDataForModel(self, model):
327        # data model
328        all_data = {}
329        for i in range(model.rowCount()):
330            properties = {}
331            item = model.item(i)
332            data = GuiUtils.dataFromItem(item)
333            if data is None: continue
334            # Now, all plots under this item
335            filename = data.filename
336            is_checked = item.checkState()
337            properties['checked'] = is_checked
338            other_datas = []
339            # no need to save other_datas - things will be refit on read
340            #other_datas = GuiUtils.plotsFromFilename(filename, model)
341            # skip the main plot
342            #other_datas = list(other_datas.values())[1:]
343            all_data[data.id] = [data, properties, other_datas]
344        return all_data
345
346    def getDataForID(self, id):
347        # return the dataset with the given ID
348        all_data = []
349        for model in (self.model, self.theory_model):
350            for i in range(model.rowCount()):
351                properties = {}
352                item = model.item(i)
353                data = GuiUtils.dataFromItem(item)
354                if data is None: continue
355                if data.id != id: continue
356                # We found the dataset - save it.
357                filename = data.filename
358                is_checked = item.checkState()
359                properties['checked'] = is_checked
360                other_datas = GuiUtils.plotsFromFilename(filename, model)
361                # skip the main plot
362                other_datas = list(other_datas.values())[1:]
363                all_data = [data, properties, other_datas]
364                break
365        return all_data
366
367    def getItemForID(self, id):
368        # return the model item with the given ID
369        item = None
370        for model in (self.model, self.theory_model):
371            for i in range(model.rowCount()):
372                properties = {}
373                item = model.item(i)
374                data = GuiUtils.dataFromItem(item)
375                if data is None: continue
376                if data.id != id: continue
377                # We found the item - return it
378                break
379        return item
380
381    def getAllData(self):
382        """
383        converts all datasets into serializable dictionary
384        """
385        data = self.allDataForModel(self.model)
386        theory = self.allDataForModel(self.theory_model)
387
388        all_data = {}
389        all_data['is_batch'] = str(self.chkBatch.isChecked())
390
391        for key, value in data.items():
392            all_data[key] = value
393        for key, value in theory.items():
394            if key in all_data:
395                raise ValueError("Inconsistent data in Project file.")
396            all_data[key] = value
397        return all_data
398
399    def saveDataToFile(self, outfile):
400        """
401        Save every dataset to a json file
402        """
403        all_data = self.getAllData()
404        # save datas
405        GuiUtils.saveData(outfile, all_data)
406
407    def readProject(self, filename):
408        """
409        Read out datasets and fitpages from file
410        """
411        # Find out the filetype based on extension
412        ext = os.path.splitext(filename)[1]
413        all_data = {}
414        if 'svs' in ext.lower():
415            # backward compatibility mode.
416            try:
417                datasets = GuiUtils.readProjectFromSVS(filename)
418            except Exception as ex:
419                # disregard malformed SVS and try to recover whatever
420                # is available
421                msg = "Error while reading the project file: "+str(ex)
422                logging.error(msg)
423                pass
424            # Convert fitpage properties and update the dict
425            try:
426                all_data = GuiUtils.convertFromSVS(datasets)
427            except Exception as ex:
428                # disregard malformed SVS and try to recover regardless
429                msg = "Error while converting the project file: "+str(ex)
430                logging.error(msg)
431                pass
432        else:
433            with open(filename, 'r') as infile:
434                try:
435                    all_data = GuiUtils.readDataFromFile(infile)
436                except Exception as ex:
437                    logging.error("Project load failed with " + str(ex))
438                    return
439        for key, value in all_data.items():
440            if key=='is_batch':
441                self.chkBatch.setChecked(True if value=='True' else False)
442                continue
443            # send newly created items to the perspective
444            self.updatePerspectiveWithProperties(key, value)
445
446        # See if there are any batch pages defined and create them, if so
447        self.updateWithBatchPages(all_data)
448
449    def updateWithBatchPages(self, all_data):
450        """
451        Checks all properties and see if there are any batch pages defined.
452        If so, pull out relevant indices and recreate the batch page(s)
453        """
454        batch_pages = []
455        for key, value in all_data.items():
456            if 'fit_params' not in value:
457                continue
458            params = value['fit_params']
459            for page in params:
460                if page['is_batch_fitting'][0] != 'True':
461                    continue
462                batch_ids = page['data_id'][0]
463                # check for duplicates
464                batch_set = set(batch_ids)
465                if batch_set in batch_pages:
466                    continue
467                # Found a unique batch page. Send it away
468                items = [self.getItemForID(i) for i in batch_set]
469                # Update the batch page list
470                batch_pages.append(batch_set)
471                # Assign parameters to the most recent (current) page.
472                self._perspective().setData(data_item=items, is_batch=True)
473                self._perspective().updateFromParameters(page)
474
475        pass
476
477    def updatePerspectiveWithProperties(self, key, value):
478        """
479        """
480        data_dict = {key:value['fit_data']}
481        # Create new model items in the data explorer
482        items = self.updateModelFromData(data_dict)
483
484        if 'fit_params' in value:
485            params = value['fit_params']
486            # Make the perspective read the rest of the read data
487            if not isinstance(params, list):
488                params = [params]
489            for page in params:
490                # Check if this set of parameters is for a batch page
491                # if so, skip the update
492                if page['is_batch_fitting'][0] == 'True':
493                    continue
494                # Send current model item to the perspective
495                self.sendItemToPerspective(items[0])
496                # Assign parameters to the most recent (current) page.
497                self._perspective().updateFromParameters(page)
498        pass # debugger
499
500    def updateModelFromData(self, data):
501        """
502        Given data from analysis/project file,
503        create indices and populate data/theory models
504        """
505        # model items for top level datasets
506        items = []
507        for key, value in data.items():
508            # key - cardinal number of dataset
509            # value - main dataset, [dependant filesets]
510            # add the main index
511            if not value: continue
512            #if key=='is_batch':
513            #    self.chkBatch.setChecked(True if value=='True' else False)
514            #    continue
515            new_data = value[0]
516            from sas.sascalc.dataloader.data_info import Data1D as old_data1d
517            from sas.sascalc.dataloader.data_info import Data2D as old_data2d
518            if isinstance(new_data, (old_data1d, old_data2d)):
519                new_data = self.manager.create_gui_data(value[0], new_data.filename)
520            assert isinstance(new_data, (Data1D, Data2D))
521            properties = value[1]
522            is_checked = properties['checked']
523            new_item = GuiUtils.createModelItemWithPlot(new_data, new_data.filename)
524            new_item.setCheckState(is_checked)
525            items.append(new_item)
526            model = self.theory_model
527            if new_data.is_data:
528                model = self.model
529                # Caption for the theories
530                new_item.setChild(2, QtGui.QStandardItem("FIT RESULTS"))
531
532            model.appendRow(new_item)
533            self.manager.add_data(data_list={new_data.id:new_data})
534
535            # Add the underlying data
536            if not value[2]:
537                continue
538            for plot in value[2]:
539                assert isinstance(plot, (Data1D, Data2D))
540                GuiUtils.updateModelItemWithPlot(new_item, plot, plot.name)
541        return items
542
543    def deleteFile(self, event):
544        """
545        Delete selected rows from the model
546        """
547        # Assure this is indeed wanted
548        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
549                     "\nDo you want to continue?"
550        reply = QtWidgets.QMessageBox.question(self,
551                                           'Warning',
552                                           delete_msg,
553                                           QtWidgets.QMessageBox.Yes,
554                                           QtWidgets.QMessageBox.No)
555
556        if reply == QtWidgets.QMessageBox.No:
557            return
558
559        # Figure out which rows are checked
560        ind = -1
561        # Use 'while' so the row count is forced at every iteration
562        deleted_items = []
563        deleted_names = []
564        while ind < self.model.rowCount():
565            ind += 1
566            item = self.model.item(ind)
567
568            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
569                # Delete these rows from the model
570                deleted_names.append(str(self.model.item(ind).text()))
571                deleted_items.append(item)
572
573                self.model.removeRow(ind)
574                # Decrement index since we just deleted it
575                ind -= 1
576
577        # Let others know we deleted data
578        self.communicator.dataDeletedSignal.emit(deleted_items)
579
580        # update stored_data
581        self.manager.update_stored_data(deleted_names)
582
583    def deleteTheory(self, event):
584        """
585        Delete selected rows from the theory model
586        """
587        # Assure this is indeed wanted
588        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
589                     "\nDo you want to continue?"
590        reply = QtWidgets.QMessageBox.question(self,
591                                           'Warning',
592                                           delete_msg,
593                                           QtWidgets.QMessageBox.Yes,
594                                           QtWidgets.QMessageBox.No)
595
596        if reply == QtWidgets.QMessageBox.No:
597            return
598
599        # Figure out which rows are checked
600        ind = -1
601        # Use 'while' so the row count is forced at every iteration
602        while ind < self.theory_model.rowCount():
603            ind += 1
604            item = self.theory_model.item(ind)
605            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
606                # Delete these rows from the model
607                self.theory_model.removeRow(ind)
608                # Decrement index since we just deleted it
609                ind -= 1
610
611        # pass temporarily kept as a breakpoint anchor
612        pass
613
614    def sendData(self, event=None):
615        """
616        Send selected item data to the current perspective and set the relevant notifiers
617        """
618        # Set the signal handlers
619        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
620
621        def isItemReady(index):
622            item = self.model.item(index)
623            return item.isCheckable() and item.checkState() == QtCore.Qt.Checked
624
625        # Figure out which rows are checked
626        selected_items = [self.model.item(index)
627                          for index in range(self.model.rowCount())
628                          if isItemReady(index)]
629
630        if len(selected_items) < 1:
631            return
632
633        # Which perspective has been selected?
634        if len(selected_items) > 1 and not self._perspective().allowBatch():
635            if hasattr(self._perspective(), 'title'):
636                title = self._perspective().title()
637            else:
638                title = self._perspective().windowTitle()
639            msg = title + " does not allow multiple data."
640            msgbox = QtWidgets.QMessageBox()
641            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
642            msgbox.setText(msg)
643            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
644            retval = msgbox.exec_()
645            return
646
647        # Notify the GuiManager about the send request
648        try:
649            self._perspective().setData(data_item=selected_items, is_batch=self.chkBatch.isChecked())
650        except Exception as ex:
651            msg = "%s perspective returned the following message: \n%s\n" %(self._perspective().name, str(ex))
652            logging.error(msg)
653            msg = str(ex)
654            msgbox = QtWidgets.QMessageBox()
655            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
656            msgbox.setText(msg)
657            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
658            retval = msgbox.exec_()
659
660    def sendItemToPerspective(self, item):
661        """
662        Send the passed item data to the current perspective and set the relevant notifiers
663        """
664        # Set the signal handlers
665        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
666        selected_items = [item]
667        # Notify the GuiManager about the send request
668        try:
669            self._perspective().setData(data_item=selected_items, is_batch=False)
670        except Exception as ex:
671            msg = "%s perspective returned the following message: \n%s\n" %(self._perspective().name, str(ex))
672            logging.error(msg)
673            msg = str(ex)
674            msgbox = QtWidgets.QMessageBox()
675            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
676            msgbox.setText(msg)
677            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
678            retval = msgbox.exec_()
679
680    def freezeCheckedData(self):
681        """
682        Convert checked results (fitted model, residuals) into separate dataset.
683        """
684        outer_index = -1
685        theories_copied = 0
686        orig_model_size = self.model.rowCount()
687        while outer_index < orig_model_size:
688            outer_index += 1
689            outer_item = self.model.item(outer_index)
690            if not outer_item:
691                continue
692            if not outer_item.isCheckable():
693                continue
694            # Look for checked inner items
695            inner_index = -1
696            while inner_index < outer_item.rowCount():
697               inner_item = outer_item.child(inner_index)
698               inner_index += 1
699               if not inner_item:
700                   continue
701               if not inner_item.isCheckable():
702                   continue
703               if inner_item.checkState() != QtCore.Qt.Checked:
704                   continue
705               self.model.beginResetModel()
706               theories_copied += 1
707               new_item = self.cloneTheory(inner_item)
708               self.model.appendRow(new_item)
709               self.model.endResetModel()
710
711        freeze_msg = ""
712        if theories_copied == 0:
713            return
714        elif theories_copied == 1:
715            freeze_msg = "1 theory copied to a separate data set"
716        elif theories_copied > 1:
717            freeze_msg = "%i theories copied to separate data sets" % theories_copied
718        else:
719            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
720            raise AttributeError(freeze_msg)
721        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
722
723    def freezeTheory(self, event):
724        """
725        Freeze selected theory rows.
726
727        "Freezing" means taking the plottable data from the Theory item
728        and copying it to a separate top-level item in Data.
729        """
730        # Figure out which rows are checked
731        # Use 'while' so the row count is forced at every iteration
732        outer_index = -1
733        theories_copied = 0
734        while outer_index < self.theory_model.rowCount():
735            outer_index += 1
736            outer_item = self.theory_model.item(outer_index)
737            if not outer_item:
738                continue
739            if outer_item.isCheckable() and \
740                   outer_item.checkState() == QtCore.Qt.Checked:
741                self.model.beginResetModel()
742                theories_copied += 1
743                new_item = self.cloneTheory(outer_item)
744                self.model.appendRow(new_item)
745                self.model.endResetModel()
746
747        freeze_msg = ""
748        if theories_copied == 0:
749            return
750        elif theories_copied == 1:
751            freeze_msg = "1 theory copied from the Theory tab as a data set"
752        elif theories_copied > 1:
753            freeze_msg = "%i theories copied from the Theory tab as data sets" % theories_copied
754        else:
755            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
756            raise AttributeError(freeze_msg)
757        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
758        # Actively switch tabs
759        self.setCurrentIndex(1)
760
761    def cloneTheory(self, item_from):
762        """
763        Manually clone theory items into a new HashableItem
764        """
765        new_item = GuiUtils.HashableStandardItem()
766        new_item.setCheckable(True)
767        new_item.setCheckState(QtCore.Qt.Checked)
768        info_item = QtGui.QStandardItem("Info")
769        data_item = QtGui.QStandardItem()
770        data_item.setData(item_from.child(0).data())
771        new_item.setText(item_from.text())
772        new_item.setChild(0, data_item)
773        new_item.setChild(1, info_item)
774        # Append a "unique" descriptor to the name
775        time_bit = str(time.time())[7:-1].replace('.', '')
776        new_name = new_item.text() + '_@' + time_bit
777        new_item.setText(new_name)
778        # Change the underlying data so it is no longer a theory
779        try:
780            new_item.child(0).data().is_data = True
781            new_item.child(0).data().symbol = 'Circle'
782        except AttributeError:
783            #no data here, pass
784            pass
785        return new_item
786
787    def recursivelyCloneItem(self, item):
788        """
789        Clone QStandardItem() object
790        """
791        new_item = item.clone()
792        # clone doesn't do deepcopy :(
793        for child_index in range(item.rowCount()):
794            child_item = self.recursivelyCloneItem(item.child(child_index))
795            new_item.setChild(child_index, child_item)
796        return new_item
797
798    def updatePlotName(self, name_tuple):
799        """
800        Modify the name of the current plot
801        """
802        old_name, current_name = name_tuple
803        ind = self.cbgraph.findText(old_name)
804        self.cbgraph.setCurrentIndex(ind)
805        self.cbgraph.setItemText(ind, current_name)
806
807    def add_data(self, data_list):
808        """
809        Update the data manager with new items
810        """
811        self.manager.add_data(data_list)
812
813    def updateGraphCount(self, graph_list):
814        """
815        Modify the graph name combo and potentially remove
816        deleted graphs
817        """
818        self.updateGraphCombo(graph_list)
819
820        if not self.active_plots:
821            return
822        new_plots = [PlotHelper.plotById(plot) for plot in graph_list]
823        active_plots_copy = list(self.active_plots.keys())
824        for plot in active_plots_copy:
825            if self.active_plots[plot] in new_plots:
826                continue
827            self.active_plots.pop(plot)
828
829    def updateGraphCombo(self, graph_list):
830        """
831        Modify Graph combo box on graph add/delete
832        """
833        orig_text = self.cbgraph.currentText()
834        self.cbgraph.clear()
835        self.cbgraph.insertItems(0, graph_list)
836        ind = self.cbgraph.findText(orig_text)
837        if ind > 0:
838            self.cbgraph.setCurrentIndex(ind)
839
840    def updatePerspectiveCombo(self, index):
841        """
842        Notify the gui manager about the new perspective chosen.
843        """
844        self.communicator.perspectiveChangedSignal.emit(self.cbFitting.itemText(index))
845        self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
846
847    def itemFromFilename(self, filename):
848        """
849        Retrieves model item corresponding to the given filename
850        """
851        item = GuiUtils.itemFromFilename(filename, self.model)
852        return item
853
854    def displayFile(self, filename=None, is_data=True, id=None):
855        """
856        Forces display of charts for the given filename
857        """
858        model = self.model if is_data else self.theory_model
859        # Now query the model item for available plots
860        plots = GuiUtils.plotsFromFilename(filename, model)
861        # Each fitpage contains the name based on fit widget number
862        fitpage_name = "" if id is None else "M"+str(id)
863        new_plots = []
864        for item, plot in plots.items():
865            if self.updatePlot(plot):
866                # Don't create plots which are already displayed
867                continue
868            # Don't plot intermediate results, e.g. P(Q), S(Q)
869            match = GuiUtils.theory_plot_ID_pattern.match(plot.id)
870            # 2nd match group contains the identifier for the intermediate
871            # result, if present (e.g. "[P(Q)]")
872            if match and match.groups()[1] != None:
873                continue
874            # Don't include plots from different fitpages,
875            # but always include the original data
876            if (fitpage_name in plot.name
877                    or filename in plot.name
878                    or filename == plot.filename):
879                # Residuals get their own plot
880                if plot.plot_role == Data1D.ROLE_RESIDUAL:
881                    plot.yscale='linear'
882                    self.plotData([(item, plot)])
883                else:
884                    new_plots.append((item, plot))
885
886        if new_plots:
887            self.plotData(new_plots)
888
889    def displayData(self, data_list, id=None):
890        """
891        Forces display of charts for the given data set
892        """
893        # data_list = [QStandardItem, Data1D/Data2D]
894        plot_to_show = data_list[1]
895        plot_item = data_list[0]
896
897        # plots to show
898        new_plots = []
899
900        # Get the main data plot
901        main_data = GuiUtils.dataFromItem(plot_item.parent())
902        if main_data is None:
903            # Try the current item
904            main_data = GuiUtils.dataFromItem(plot_item)
905
906        # Make sure main data for 2D is always displayed
907        if main_data and not self.isPlotShown(main_data):
908            if isinstance(main_data, Data2D):
909                self.plotData([(plot_item, main_data)])
910
911        # Check if this is merely a plot update
912        if self.updatePlot(plot_to_show):
913            return
914
915        # Residuals get their own plot
916        if plot_to_show.plot_role == Data1D.ROLE_RESIDUAL:
917            plot_to_show.yscale='linear'
918            self.plotData([(plot_item, plot_to_show)])
919        elif plot_to_show.plot_role == Data1D.ROLE_DELETABLE:
920            # No plot
921            return
922        else:
923            # Plots with main data points on the same chart
924            # Get the main data plot
925            if main_data and not self.isPlotShown(main_data):
926                new_plots.append((plot_item, main_data))
927            new_plots.append((plot_item, plot_to_show))
928
929        if new_plots:
930            self.plotData(new_plots)
931
932    def isPlotShown(self, plot):
933        """
934        Checks currently shown plots and returns true if match
935        """
936        if not hasattr(plot, 'name'):
937            return False
938        ids_vals = [val.data.name for val in self.active_plots.values()]
939
940        return plot.name in ids_vals
941
942    def addDataPlot2D(self, plot_set, item):
943        """
944        Create a new 2D plot and add it to the workspace
945        """
946        plot2D = Plotter2D(self)
947        plot2D.item = item
948        plot2D.plot(plot_set)
949        self.addPlot(plot2D)
950        self.active_plots[plot2D.data.name] = plot2D
951        #============================================
952        # Experimental hook for silx charts
953        #============================================
954        ## Attach silx
955        #from silx.gui import qt
956        #from silx.gui.plot import StackView
957        #sv = StackView()
958        #sv.setColormap("jet", autoscale=True)
959        #sv.setStack(plot_set.data.reshape(1,100,100))
960        ##sv.setLabels(["x: -10 to 10 (200 samples)",
961        ##              "y: -10 to 5 (150 samples)"])
962        #sv.show()
963        #============================================
964
965    def plotData(self, plots, transform=True):
966        """
967        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
968        """
969        # Call show on requested plots
970        # All same-type charts in one plot
971        for item, plot_set in plots:
972            if isinstance(plot_set, Data1D):
973                if not 'new_plot' in locals():
974                    new_plot = Plotter(self)
975                    new_plot.item = item
976                new_plot.plot(plot_set, transform=transform)
977                # active_plots may contain multiple charts
978                self.active_plots[plot_set.name] = new_plot
979            elif isinstance(plot_set, Data2D):
980                self.addDataPlot2D(plot_set, item)
981            else:
982                msg = "Incorrect data type passed to Plotting"
983                raise AttributeError(msg)
984
985        if 'new_plot' in locals() and \
986            hasattr(new_plot, 'data') and \
987            isinstance(new_plot.data, Data1D):
988                self.addPlot(new_plot)
989
990    def newPlot(self):
991        """
992        Select checked data and plot it
993        """
994        # Check which tab is currently active
995        if self.current_view == self.treeView:
996            plots = GuiUtils.plotsFromCheckedItems(self.model)
997        else:
998            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
999
1000        self.plotData(plots)
1001
1002    def addPlot(self, new_plot):
1003        """
1004        Helper method for plot bookkeeping
1005        """
1006        # Update the global plot counter
1007        title = str(PlotHelper.idOfPlot(new_plot))
1008        new_plot.setWindowTitle(title)
1009
1010        # Set the object name to satisfy the Squish object picker
1011        new_plot.setObjectName(title)
1012
1013        # Add the plot to the workspace
1014        plot_widget = self.parent.workspace().addSubWindow(new_plot)
1015
1016        # Show the plot
1017        new_plot.show()
1018        new_plot.canvas.draw()
1019
1020        # Update the plot widgets dict
1021        self.plot_widgets[title]=plot_widget
1022
1023        # Update the active chart list
1024        self.active_plots[new_plot.data.name] = new_plot
1025
1026    def appendPlot(self):
1027        """
1028        Add data set(s) to the existing matplotlib chart
1029        """
1030        # new plot data; check which tab is currently active
1031        if self.current_view == self.treeView:
1032            new_plots = GuiUtils.plotsFromCheckedItems(self.model)
1033        else:
1034            new_plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
1035
1036        # old plot data
1037        plot_id = str(self.cbgraph.currentText())
1038        try:
1039            assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
1040        except:
1041            return
1042
1043        old_plot = PlotHelper.plotById(plot_id)
1044
1045        # Add new data to the old plot, if data type is the same.
1046        for _, plot_set in new_plots:
1047            if type(plot_set) is type(old_plot._data):
1048                old_plot.data = plot_set
1049                old_plot.plot()
1050                # need this for lookup - otherwise this plot will never update
1051                self.active_plots[plot_set.name] = old_plot
1052
1053    def updatePlot(self, data):
1054        """
1055        Modify existing plot for immediate response and returns True.
1056        Returns false, if the plot does not exist already.
1057        """
1058        try: # there might be a list or a single value being passed
1059            data = data[0]
1060        except TypeError:
1061            pass
1062        assert type(data).__name__ in ['Data1D', 'Data2D']
1063
1064        ids_keys = list(self.active_plots.keys())
1065        ids_vals = [val.data.name for val in self.active_plots.values()]
1066
1067        data_id = data.name
1068        if data_id in ids_keys:
1069            # We have data, let's replace data that needs replacing
1070            if data.plot_role != Data1D.ROLE_DATA:
1071                self.active_plots[data_id].replacePlot(data_id, data)
1072            return True
1073        elif data_id in ids_vals:
1074            if data.plot_role != Data1D.ROLE_DATA:
1075                list(self.active_plots.values())[ids_vals.index(data_id)].replacePlot(data_id, data)
1076            return True
1077        return False
1078
1079    def chooseFiles(self):
1080        """
1081        Shows the Open file dialog and returns the chosen path(s)
1082        """
1083        # List of known extensions
1084        wlist = self.getWlist()
1085
1086        # Location is automatically saved - no need to keep track of the last dir
1087        # But only with Qt built-in dialog (non-platform native)
1088        paths = QtWidgets.QFileDialog.getOpenFileNames(self, "Choose a file", "",
1089                wlist, None, QtWidgets.QFileDialog.DontUseNativeDialog)[0]
1090        if not paths:
1091            return
1092
1093        if not isinstance(paths, list):
1094            paths = [paths]
1095
1096        return paths
1097
1098    def readData(self, path):
1099        """
1100        verbatim copy-paste from
1101           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
1102        slightly modified for clarity
1103        """
1104        message = ""
1105        log_msg = ''
1106        output = {}
1107        any_error = False
1108        data_error = False
1109        error_message = ""
1110        number_of_files = len(path)
1111        self.communicator.progressBarUpdateSignal.emit(0.0)
1112
1113        for index, p_file in enumerate(path):
1114            basename = os.path.basename(p_file)
1115            _, extension = os.path.splitext(basename)
1116            if extension.lower() in GuiUtils.EXTENSIONS:
1117                any_error = True
1118                log_msg = "Data Loader cannot "
1119                log_msg += "load: %s\n" % str(p_file)
1120                log_msg += """Please try to open that file from "open project" """
1121                log_msg += """or "open analysis" menu\n"""
1122                error_message = log_msg + "\n"
1123                logging.info(log_msg)
1124                continue
1125
1126            try:
1127                message = "Loading Data... " + str(basename) + "\n"
1128
1129                # change this to signal notification in GuiManager
1130                self.communicator.statusBarUpdateSignal.emit(message)
1131
1132                output_objects = self.loader.load(p_file)
1133
1134                # Some loaders return a list and some just a single Data1D object.
1135                # Standardize.
1136                if not isinstance(output_objects, list):
1137                    output_objects = [output_objects]
1138
1139                for item in output_objects:
1140                    # cast sascalc.dataloader.data_info.Data1D into
1141                    # sasgui.guiframe.dataFitting.Data1D
1142                    # TODO : Fix it
1143                    new_data = self.manager.create_gui_data(item, p_file)
1144                    output[new_data.id] = new_data
1145
1146                    # Model update should be protected
1147                    self.mutex.lock()
1148                    self.updateModel(new_data, p_file)
1149                    #self.model.reset()
1150                    QtWidgets.QApplication.processEvents()
1151                    self.mutex.unlock()
1152
1153                    if hasattr(item, 'errors'):
1154                        for error_data in item.errors:
1155                            data_error = True
1156                            message += "\tError: {0}\n".format(error_data)
1157                    else:
1158
1159                        logging.error("Loader returned an invalid object:\n %s" % str(item))
1160                        data_error = True
1161
1162            except Exception as ex:
1163                logging.error(sys.exc_info()[1])
1164
1165                any_error = True
1166            if any_error or error_message != "":
1167                if error_message == "":
1168                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
1169                    error += "while loading Data: \n%s\n" % str(basename)
1170                    error_message += "The data file you selected could not be loaded.\n"
1171                    error_message += "Make sure the content of your file"
1172                    error_message += " is properly formatted.\n\n"
1173                    error_message += "When contacting the SasView team, mention the"
1174                    error_message += " following:\n%s" % str(error)
1175                elif data_error:
1176                    base_message = "Errors occurred while loading "
1177                    base_message += "{0}\n".format(basename)
1178                    base_message += "The data file loaded but with errors.\n"
1179                    error_message = base_message + error_message
1180                else:
1181                    error_message += "%s\n" % str(p_file)
1182
1183            current_percentage = int(100.0* index/number_of_files)
1184            self.communicator.progressBarUpdateSignal.emit(current_percentage)
1185
1186        if any_error or error_message:
1187            logging.error(error_message)
1188            status_bar_message = "Errors occurred while loading %s" % format(basename)
1189            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
1190
1191        else:
1192            message = "Loading Data Complete! "
1193        message += log_msg
1194        # Notify the progress bar that the updates are over.
1195        self.communicator.progressBarUpdateSignal.emit(-1)
1196        self.communicator.statusBarUpdateSignal.emit(message)
1197
1198        return output, message
1199
1200    def getWlist(self):
1201        """
1202        Wildcards of files we know the format of.
1203        """
1204        # Display the Qt Load File module
1205        cards = self.loader.get_wildcards()
1206
1207        # get rid of the wx remnant in wildcards
1208        # TODO: modify sasview loader get_wildcards method, after merge,
1209        # so this kludge can be avoided
1210        new_cards = []
1211        for item in cards:
1212            new_cards.append(item[:item.find("|")])
1213        wlist = ';;'.join(new_cards)
1214
1215        return wlist
1216
1217    def setItemsCheckability(self, model, dimension=None, checked=False):
1218        """
1219        For a given model, check or uncheck all items of given dimension
1220        """
1221        mode = QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked
1222
1223        assert isinstance(checked, bool)
1224
1225        types = (None, Data1D, Data2D)
1226        if not dimension in types:
1227            return
1228
1229        for index in range(model.rowCount()):
1230            item = model.item(index)
1231            if item.isCheckable() and item.checkState() != mode:
1232                data = item.child(0).data()
1233                if dimension is None or isinstance(data, dimension):
1234                    item.setCheckState(mode)
1235
1236            items = list(GuiUtils.getChildrenFromItem(item))
1237
1238            for it in items:
1239                if it.isCheckable() and it.checkState() != mode:
1240                    data = it.child(0).data()
1241                    if dimension is None or isinstance(data, dimension):
1242                        it.setCheckState(mode)
1243
1244    def selectData(self, index):
1245        """
1246        Callback method for modifying the TreeView on Selection Options change
1247        """
1248        if not isinstance(index, int):
1249            msg = "Incorrect type passed to DataExplorer.selectData()"
1250            raise AttributeError(msg)
1251
1252        # Respond appropriately
1253        if index == 0:
1254            self.setItemsCheckability(self.model, checked=True)
1255
1256        elif index == 1:
1257            # De-select All
1258            self.setItemsCheckability(self.model, checked=False)
1259
1260        elif index == 2:
1261            # Select All 1-D
1262            self.setItemsCheckability(self.model, dimension=Data1D, checked=True)
1263
1264        elif index == 3:
1265            # Unselect All 1-D
1266            self.setItemsCheckability(self.model, dimension=Data1D, checked=False)
1267
1268        elif index == 4:
1269            # Select All 2-D
1270            self.setItemsCheckability(self.model, dimension=Data2D, checked=True)
1271
1272        elif index == 5:
1273            # Unselect All 2-D
1274            self.setItemsCheckability(self.model, dimension=Data2D, checked=False)
1275
1276        else:
1277            msg = "Incorrect value in the Selection Option"
1278            # Change this to a proper logging action
1279            raise Exception(msg)
1280
1281    def contextMenu(self):
1282        """
1283        Define actions and layout of the right click context menu
1284        """
1285        # Create a custom menu based on actions defined in the UI file
1286        self.context_menu = QtWidgets.QMenu(self)
1287        self.context_menu.addAction(self.actionDataInfo)
1288        self.context_menu.addAction(self.actionSaveAs)
1289        self.context_menu.addAction(self.actionQuickPlot)
1290        self.context_menu.addSeparator()
1291        self.context_menu.addAction(self.actionQuick3DPlot)
1292        self.context_menu.addAction(self.actionEditMask)
1293        self.context_menu.addSeparator()
1294        self.context_menu.addAction(self.actionFreezeResults)
1295        self.context_menu.addSeparator()
1296        self.context_menu.addAction(self.actionDelete)
1297
1298
1299        # Define the callbacks
1300        self.actionDataInfo.triggered.connect(self.showDataInfo)
1301        self.actionSaveAs.triggered.connect(self.saveDataAs)
1302        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
1303        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
1304        self.actionEditMask.triggered.connect(self.showEditDataMask)
1305        self.actionDelete.triggered.connect(self.deleteSelectedItem)
1306        self.actionFreezeResults.triggered.connect(self.freezeSelectedItems)
1307
1308    def onCustomContextMenu(self, position):
1309        """
1310        Show the right-click context menu in the data treeview
1311        """
1312        index = self.current_view.indexAt(position)
1313        proxy = self.current_view.model()
1314        model = proxy.sourceModel()
1315
1316        if not index.isValid():
1317            return
1318        model_item = model.itemFromIndex(proxy.mapToSource(index))
1319        # Find the mapped index
1320        orig_index = model_item.isCheckable()
1321        if not orig_index:
1322            return
1323        # Check the data to enable/disable actions
1324        is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
1325        self.actionQuick3DPlot.setEnabled(is_2D)
1326        self.actionEditMask.setEnabled(is_2D)
1327
1328        # Freezing
1329        # check that the selection has inner items
1330        freeze_enabled = False
1331        if model_item.parent() is not None:
1332            freeze_enabled = True
1333        self.actionFreezeResults.setEnabled(freeze_enabled)
1334
1335        # Fire up the menu
1336        self.context_menu.exec_(self.current_view.mapToGlobal(position))
1337
1338    def showDataInfo(self):
1339        """
1340        Show a simple read-only text edit with data information.
1341        """
1342        index = self.current_view.selectedIndexes()[0]
1343        proxy = self.current_view.model()
1344        model = proxy.sourceModel()
1345        model_item = model.itemFromIndex(proxy.mapToSource(index))
1346
1347        data = GuiUtils.dataFromItem(model_item)
1348        if isinstance(data, Data1D):
1349            text_to_show = GuiUtils.retrieveData1d(data)
1350            # Hardcoded sizes to enable full width rendering with default font
1351            self.txt_widget.resize(420,600)
1352        else:
1353            text_to_show = GuiUtils.retrieveData2d(data)
1354            # Hardcoded sizes to enable full width rendering with default font
1355            self.txt_widget.resize(700,600)
1356
1357        self.txt_widget.setReadOnly(True)
1358        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
1359        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
1360        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
1361        self.txt_widget.clear()
1362        self.txt_widget.insertPlainText(text_to_show)
1363
1364        self.txt_widget.show()
1365        # Move the slider all the way up, if present
1366        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
1367        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
1368
1369    def saveDataAs(self):
1370        """
1371        Save the data points as either txt or xml
1372        """
1373        index = self.current_view.selectedIndexes()[0]
1374        proxy = self.current_view.model()
1375        model = proxy.sourceModel()
1376        model_item = model.itemFromIndex(proxy.mapToSource(index))
1377
1378        data = GuiUtils.dataFromItem(model_item)
1379        if isinstance(data, Data1D):
1380            GuiUtils.saveData1D(data)
1381        else:
1382            GuiUtils.saveData2D(data)
1383
1384    def quickDataPlot(self):
1385        """
1386        Frozen plot - display an image of the plot
1387        """
1388        index = self.current_view.selectedIndexes()[0]
1389        proxy = self.current_view.model()
1390        model = proxy.sourceModel()
1391        model_item = model.itemFromIndex(proxy.mapToSource(index))
1392
1393        data = GuiUtils.dataFromItem(model_item)
1394
1395        method_name = 'Plotter'
1396        if isinstance(data, Data2D):
1397            method_name='Plotter2D'
1398
1399        self.new_plot = globals()[method_name](self, quickplot=True)
1400        self.new_plot.data = data
1401        #new_plot.plot(marker='o')
1402        self.new_plot.plot()
1403
1404        # Update the global plot counter
1405        title = "Plot " + data.name
1406        self.new_plot.setWindowTitle(title)
1407
1408        # Show the plot
1409        self.new_plot.show()
1410
1411    def quickData3DPlot(self):
1412        """
1413        Slowish 3D plot
1414        """
1415        index = self.current_view.selectedIndexes()[0]
1416        proxy = self.current_view.model()
1417        model = proxy.sourceModel()
1418        model_item = model.itemFromIndex(proxy.mapToSource(index))
1419
1420        data = GuiUtils.dataFromItem(model_item)
1421
1422        self.new_plot = Plotter2D(self, quickplot=True, dimension=3)
1423        self.new_plot.data = data
1424        self.new_plot.plot()
1425
1426        # Update the global plot counter
1427        title = "Plot " + data.name
1428        self.new_plot.setWindowTitle(title)
1429
1430        # Show the plot
1431        self.new_plot.show()
1432
1433    def extShowEditDataMask(self):
1434        self.showEditDataMask()
1435
1436    def showEditDataMask(self, data=None):
1437        """
1438        Mask Editor for 2D plots
1439        """
1440        msg = QtWidgets.QMessageBox()
1441        msg.setIcon(QtWidgets.QMessageBox.Information)
1442        msg.setText("Error: cannot apply mask.\n"+
1443                    "Please select a 2D dataset.")
1444        msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
1445
1446        try:
1447            if data is None or not isinstance(data, Data2D):
1448                # if data wasn't passed - try to get it from
1449                # the currently selected item
1450                index = self.current_view.selectedIndexes()[0]
1451                proxy = self.current_view.model()
1452                model = proxy.sourceModel()
1453                model_item = model.itemFromIndex(proxy.mapToSource(index))
1454
1455                data = GuiUtils.dataFromItem(model_item)
1456
1457            if data is None or not isinstance(data, Data2D):
1458                # If data is still not right, complain
1459                msg.exec_()
1460                return
1461        except:
1462            msg.exec_()
1463            return
1464
1465        mask_editor = MaskEditor(self, data)
1466        # Modal dialog here.
1467        mask_editor.exec_()
1468
1469    def freezeItem(self, item=None):
1470        """
1471        Freeze given item
1472        """
1473        if item is None:
1474            return
1475        self.model.beginResetModel()
1476        new_item = self.cloneTheory(item)
1477        self.model.appendRow(new_item)
1478        self.model.endResetModel()
1479
1480    def freezeDataToItem(self, data=None):
1481        """
1482        Freeze given set of data to main model
1483        """
1484        if data is None:
1485            return
1486        self.model.beginResetModel()
1487        # Append a "unique" descriptor to the name
1488        time_bit = str(time.time())[7:-1].replace('.', '')
1489        new_name = data.name + '_@' + time_bit
1490        # Change the underlying data so it is no longer a theory
1491        try:
1492            data.is_data = True
1493            data.symbol = 'Circle'
1494        except AttributeError:
1495            #no data here, pass
1496            pass
1497        new_item = GuiUtils.createModelItemWithPlot(data, new_name)
1498
1499        self.model.appendRow(new_item)
1500        self.model.endResetModel()
1501
1502    def freezeSelectedItems(self):
1503        """
1504        Freeze selected items
1505        """
1506        indices = self.treeView.selectedIndexes()
1507
1508        proxy = self.treeView.model()
1509        model = proxy.sourceModel()
1510
1511        for index in indices:
1512            row_index = proxy.mapToSource(index)
1513            item_to_copy = model.itemFromIndex(row_index)
1514            if item_to_copy and item_to_copy.isCheckable():
1515                self.freezeItem(item_to_copy)
1516
1517    def deleteAllItems(self):
1518        """
1519        Deletes all datasets from both model and theory_model
1520        """
1521        deleted_items = [self.model.item(row) for row in range(self.model.rowCount())
1522                         if self.model.item(row).isCheckable()]
1523        deleted_names = [item.text() for item in deleted_items]
1524        # Let others know we deleted data
1525        self.communicator.dataDeletedSignal.emit(deleted_items)
1526        # update stored_data
1527        self.manager.update_stored_data(deleted_names)
1528
1529        # Clear the model
1530        self.model.clear()
1531
1532    def deleteSelectedItem(self):
1533        """
1534        Delete the current item
1535        """
1536        # Assure this is indeed wanted
1537        delete_msg = "This operation will delete the selected data sets " +\
1538                     "and all the dependents." +\
1539                     "\nDo you want to continue?"
1540        reply = QtWidgets.QMessageBox.question(self,
1541                                           'Warning',
1542                                           delete_msg,
1543                                           QtWidgets.QMessageBox.Yes,
1544                                           QtWidgets.QMessageBox.No)
1545
1546        if reply == QtWidgets.QMessageBox.No:
1547            return
1548
1549        indices = self.current_view.selectedIndexes()
1550        self.deleteIndices(indices)
1551
1552    def deleteIndices(self, indices):
1553        """
1554        Delete model idices from the current view
1555        """
1556        proxy = self.current_view.model()
1557        model = proxy.sourceModel()
1558
1559        deleted_items = []
1560        deleted_names = []
1561
1562        # Every time a row is removed, the indices change, so we'll just remove
1563        # rows and keep calling selectedIndexes until it returns an empty list.
1564        while len(indices) > 0:
1565            index = indices[0]
1566            #row_index = proxy.mapToSource(index)
1567            #item_to_delete = model.itemFromIndex(row_index)
1568            item_to_delete = model.itemFromIndex(index)
1569            if item_to_delete and item_to_delete.isCheckable():
1570                #row = row_index.row()
1571                row = index.row()
1572
1573                # store the deleted item details so we can pass them on later
1574                deleted_names.append(item_to_delete.text())
1575                deleted_items.append(item_to_delete)
1576
1577                # Delete corresponding open plots
1578                self.closePlotsForItem(item_to_delete)
1579
1580                if item_to_delete.parent():
1581                    # We have a child item - delete from it
1582                    item_to_delete.parent().removeRow(row)
1583                else:
1584                    # delete directly from model
1585                    model.removeRow(row)
1586            indices = self.current_view.selectedIndexes()
1587
1588        # Let others know we deleted data
1589        self.communicator.dataDeletedSignal.emit(deleted_items)
1590
1591        # update stored_data
1592        self.manager.update_stored_data(deleted_names)
1593
1594    def closeAllPlots(self):
1595        """
1596        Close all currently displayed plots
1597        """
1598
1599        for plot_id in PlotHelper.currentPlots():
1600            try:
1601                plotter = PlotHelper.plotById(plot_id)
1602                plotter.close()
1603                self.plot_widgets[plot_id].close()
1604                self.plot_widgets.pop(plot_id, None)
1605            except AttributeError as ex:
1606                logging.error("Closing of %s failed:\n %s" % (plot_id, str(ex)))
1607
1608    def closePlotsForItem(self, item):
1609        """
1610        Given standard item, close all its currently displayed plots
1611        """
1612        # item - HashableStandardItems of active plots
1613
1614        # {} -> 'Graph1' : HashableStandardItem()
1615        current_plot_items = {}
1616        for plot_name in PlotHelper.currentPlots():
1617            current_plot_items[plot_name] = PlotHelper.plotById(plot_name).item
1618
1619        # item and its hashable children
1620        items_being_deleted = []
1621        if item.rowCount() > 0:
1622            items_being_deleted = [item.child(n) for n in range(item.rowCount())
1623                                   if isinstance(item.child(n), GuiUtils.HashableStandardItem)]
1624        items_being_deleted.append(item)
1625        # Add the parent in case a child is selected
1626        if isinstance(item.parent(), GuiUtils.HashableStandardItem):
1627            items_being_deleted.append(item.parent())
1628
1629        # Compare plot items and items to delete
1630        plots_to_close = set(current_plot_items.values()) & set(items_being_deleted)
1631
1632        for plot_item in plots_to_close:
1633            for plot_name in current_plot_items.keys():
1634                if plot_item == current_plot_items[plot_name]:
1635                    plotter = PlotHelper.plotById(plot_name)
1636                    # try to delete the plot
1637                    try:
1638                        plotter.close()
1639                        #self.parent.workspace().removeSubWindow(plotter)
1640                        self.plot_widgets[plot_name].close()
1641                        self.plot_widgets.pop(plot_name, None)
1642                    except AttributeError as ex:
1643                        logging.error("Closing of %s failed:\n %s" % (plot_name, str(ex)))
1644
1645        pass # debugger anchor
1646
1647    def onAnalysisUpdate(self, new_perspective=""):
1648        """
1649        Update the perspective combo index based on passed string
1650        """
1651        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1652        self.cbFitting.blockSignals(True)
1653        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1654        self.cbFitting.blockSignals(False)
1655        pass
1656
1657    def loadComplete(self, output):
1658        """
1659        Post message to status bar and update the data manager
1660        """
1661        assert isinstance(output, tuple)
1662
1663        # Reset the model so the view gets updated.
1664        #self.model.reset()
1665        self.communicator.progressBarUpdateSignal.emit(-1)
1666
1667        output_data = output[0]
1668        message = output[1]
1669        # Notify the manager of the new data available
1670        self.communicator.statusBarUpdateSignal.emit(message)
1671        self.communicator.fileDataReceivedSignal.emit(output_data)
1672        self.manager.add_data(data_list=output_data)
1673
1674    def loadFailed(self, reason):
1675        print("File Load Failed with:\n", reason)
1676        pass
1677
1678    def updateModel(self, data, p_file):
1679        """
1680        Add data and Info fields to the model item
1681        """
1682        # Structure of the model
1683        # checkbox + basename
1684        #     |-------> Data.D object
1685        #     |-------> Info
1686        #                 |----> Title:
1687        #                 |----> Run:
1688        #                 |----> Type:
1689        #                 |----> Path:
1690        #                 |----> Process
1691        #                          |-----> process[0].name
1692        #     |-------> THEORIES
1693
1694        # Top-level item: checkbox with label
1695        checkbox_item = GuiUtils.HashableStandardItem()
1696        checkbox_item.setCheckable(True)
1697        checkbox_item.setCheckState(QtCore.Qt.Checked)
1698        if p_file is not None:
1699            checkbox_item.setText(os.path.basename(p_file))
1700
1701        # Add the actual Data1D/Data2D object
1702        object_item = GuiUtils.HashableStandardItem()
1703        object_item.setData(data)
1704
1705        checkbox_item.setChild(0, object_item)
1706
1707        # Add rows for display in the view
1708        info_item = GuiUtils.infoFromData(data)
1709
1710        # Set info_item as the first child
1711        checkbox_item.setChild(1, info_item)
1712
1713        # Caption for the theories
1714        checkbox_item.setChild(2, QtGui.QStandardItem("FIT RESULTS"))
1715
1716        # New row in the model
1717        self.model.beginResetModel()
1718        self.model.appendRow(checkbox_item)
1719        self.model.endResetModel()
1720
1721    def updateModelFromPerspective(self, model_item):
1722        """
1723        Receive an update model item from a perspective
1724        Make sure it is valid and if so, replace it in the model
1725        """
1726        # Assert the correct type
1727        if not isinstance(model_item, QtGui.QStandardItem):
1728            msg = "Wrong data type returned from calculations."
1729            raise AttributeError(msg)
1730
1731        # TODO: Assert other properties
1732
1733        # Reset the view
1734        ##self.model.reset()
1735        # Pass acting as a debugger anchor
1736        pass
1737
1738    def updateTheoryFromPerspective(self, model_item):
1739        """
1740        Receive an update theory item from a perspective
1741        Make sure it is valid and if so, replace/add in the model
1742        """
1743        # Assert the correct type
1744        if not isinstance(model_item, QtGui.QStandardItem):
1745            msg = "Wrong data type returned from calculations."
1746            raise AttributeError(msg)
1747
1748        # Check if there are any other items for this tab
1749        # If so, delete them
1750        current_tab_name = model_item.text()
1751        for current_index in range(self.theory_model.rowCount()):
1752            if current_tab_name == self.theory_model.item(current_index).text():
1753                self.theory_model.removeRow(current_index)
1754                break
1755        # send in the new item
1756        self.theory_model.appendRow(model_item)
1757
1758    def deleteIntermediateTheoryPlotsByModelID(self, model_id):
1759        """Given a model's ID, deletes all items in the theory item model which reference the same ID. Useful in the
1760        case of intermediate results disappearing when changing calculations (in which case you don't want them to be
1761        retained in the list)."""
1762        items_to_delete = []
1763        for r in range(self.theory_model.rowCount()):
1764            item = self.theory_model.item(r, 0)
1765            data = item.child(0).data()
1766            if not hasattr(data, "id"):
1767                return
1768            match = GuiUtils.theory_plot_ID_pattern.match(data.id)
1769            if match:
1770                item_model_id = match.groups()[-1]
1771                if item_model_id == model_id:
1772                    # Only delete those identified as an intermediate plot
1773                    if match.groups()[2] not in (None, ""):
1774                        items_to_delete.append(item)
1775
1776        for item in items_to_delete:
1777            self.theory_model.removeRow(item.row())
Note: See TracBrowser for help on using the repository browser.