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

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

More batchpage related functionality for Analysis save/load

  • Property mode set to 100644
File size: 65.2 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            model.appendRow(new_item)
530            self.manager.add_data(data_list={new_data.id:new_data})
531
532            # Add the underlying data
533            if not value[2]:
534                continue
535            for plot in value[2]:
536                assert isinstance(plot, (Data1D, Data2D))
537                GuiUtils.updateModelItemWithPlot(new_item, plot, plot.name)
538        return items
539
540    def deleteFile(self, event):
541        """
542        Delete selected rows from the model
543        """
544        # Assure this is indeed wanted
545        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
546                     "\nDo you want to continue?"
547        reply = QtWidgets.QMessageBox.question(self,
548                                           'Warning',
549                                           delete_msg,
550                                           QtWidgets.QMessageBox.Yes,
551                                           QtWidgets.QMessageBox.No)
552
553        if reply == QtWidgets.QMessageBox.No:
554            return
555
556        # Figure out which rows are checked
557        ind = -1
558        # Use 'while' so the row count is forced at every iteration
559        deleted_items = []
560        deleted_names = []
561        while ind < self.model.rowCount():
562            ind += 1
563            item = self.model.item(ind)
564
565            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
566                # Delete these rows from the model
567                deleted_names.append(str(self.model.item(ind).text()))
568                deleted_items.append(item)
569
570                self.model.removeRow(ind)
571                # Decrement index since we just deleted it
572                ind -= 1
573
574        # Let others know we deleted data
575        self.communicator.dataDeletedSignal.emit(deleted_items)
576
577        # update stored_data
578        self.manager.update_stored_data(deleted_names)
579
580    def deleteTheory(self, event):
581        """
582        Delete selected rows from the theory model
583        """
584        # Assure this is indeed wanted
585        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
586                     "\nDo you want to continue?"
587        reply = QtWidgets.QMessageBox.question(self,
588                                           'Warning',
589                                           delete_msg,
590                                           QtWidgets.QMessageBox.Yes,
591                                           QtWidgets.QMessageBox.No)
592
593        if reply == QtWidgets.QMessageBox.No:
594            return
595
596        # Figure out which rows are checked
597        ind = -1
598        # Use 'while' so the row count is forced at every iteration
599        while ind < self.theory_model.rowCount():
600            ind += 1
601            item = self.theory_model.item(ind)
602            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
603                # Delete these rows from the model
604                self.theory_model.removeRow(ind)
605                # Decrement index since we just deleted it
606                ind -= 1
607
608        # pass temporarily kept as a breakpoint anchor
609        pass
610
611    def sendData(self, event=None):
612        """
613        Send selected item data to the current perspective and set the relevant notifiers
614        """
615        # Set the signal handlers
616        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
617
618        def isItemReady(index):
619            item = self.model.item(index)
620            return item.isCheckable() and item.checkState() == QtCore.Qt.Checked
621
622        # Figure out which rows are checked
623        selected_items = [self.model.item(index)
624                          for index in range(self.model.rowCount())
625                          if isItemReady(index)]
626
627        if len(selected_items) < 1:
628            return
629
630        # Which perspective has been selected?
631        if len(selected_items) > 1 and not self._perspective().allowBatch():
632            if hasattr(self._perspective(), 'title'):
633                title = self._perspective().title()
634            else:
635                title = self._perspective().windowTitle()
636            msg = title + " does not allow multiple data."
637            msgbox = QtWidgets.QMessageBox()
638            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
639            msgbox.setText(msg)
640            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
641            retval = msgbox.exec_()
642            return
643
644        # Notify the GuiManager about the send request
645        try:
646            self._perspective().setData(data_item=selected_items, is_batch=self.chkBatch.isChecked())
647        except Exception as ex:
648            msg = "%s perspective returned the following message: \n%s\n" %(self._perspective().name, str(ex))
649            logging.error(msg)
650            msg = str(ex)
651            msgbox = QtWidgets.QMessageBox()
652            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
653            msgbox.setText(msg)
654            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
655            retval = msgbox.exec_()
656
657    def sendItemToPerspective(self, item):
658        """
659        Send the passed item data to the current perspective and set the relevant notifiers
660        """
661        # Set the signal handlers
662        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
663        selected_items = [item]
664        # Notify the GuiManager about the send request
665        try:
666            self._perspective().setData(data_item=selected_items, is_batch=False)
667        except Exception as ex:
668            msg = "%s perspective returned the following message: \n%s\n" %(self._perspective().name, str(ex))
669            logging.error(msg)
670            msg = str(ex)
671            msgbox = QtWidgets.QMessageBox()
672            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
673            msgbox.setText(msg)
674            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
675            retval = msgbox.exec_()
676
677    def freezeCheckedData(self):
678        """
679        Convert checked results (fitted model, residuals) into separate dataset.
680        """
681        outer_index = -1
682        theories_copied = 0
683        orig_model_size = self.model.rowCount()
684        while outer_index < orig_model_size:
685            outer_index += 1
686            outer_item = self.model.item(outer_index)
687            if not outer_item:
688                continue
689            if not outer_item.isCheckable():
690                continue
691            # Look for checked inner items
692            inner_index = -1
693            while inner_index < outer_item.rowCount():
694               inner_item = outer_item.child(inner_index)
695               inner_index += 1
696               if not inner_item:
697                   continue
698               if not inner_item.isCheckable():
699                   continue
700               if inner_item.checkState() != QtCore.Qt.Checked:
701                   continue
702               self.model.beginResetModel()
703               theories_copied += 1
704               new_item = self.cloneTheory(inner_item)
705               self.model.appendRow(new_item)
706               self.model.endResetModel()
707
708        freeze_msg = ""
709        if theories_copied == 0:
710            return
711        elif theories_copied == 1:
712            freeze_msg = "1 theory copied to a separate data set"
713        elif theories_copied > 1:
714            freeze_msg = "%i theories copied to separate data sets" % theories_copied
715        else:
716            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
717            raise AttributeError(freeze_msg)
718        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
719
720    def freezeTheory(self, event):
721        """
722        Freeze selected theory rows.
723
724        "Freezing" means taking the plottable data from the Theory item
725        and copying it to a separate top-level item in Data.
726        """
727        # Figure out which rows are checked
728        # Use 'while' so the row count is forced at every iteration
729        outer_index = -1
730        theories_copied = 0
731        while outer_index < self.theory_model.rowCount():
732            outer_index += 1
733            outer_item = self.theory_model.item(outer_index)
734            if not outer_item:
735                continue
736            if outer_item.isCheckable() and \
737                   outer_item.checkState() == QtCore.Qt.Checked:
738                self.model.beginResetModel()
739                theories_copied += 1
740                new_item = self.cloneTheory(outer_item)
741                self.model.appendRow(new_item)
742                self.model.endResetModel()
743
744        freeze_msg = ""
745        if theories_copied == 0:
746            return
747        elif theories_copied == 1:
748            freeze_msg = "1 theory copied from the Theory tab as a data set"
749        elif theories_copied > 1:
750            freeze_msg = "%i theories copied from the Theory tab as data sets" % theories_copied
751        else:
752            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
753            raise AttributeError(freeze_msg)
754        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
755        # Actively switch tabs
756        self.setCurrentIndex(1)
757
758    def cloneTheory(self, item_from):
759        """
760        Manually clone theory items into a new HashableItem
761        """
762        new_item = GuiUtils.HashableStandardItem()
763        new_item.setCheckable(True)
764        new_item.setCheckState(QtCore.Qt.Checked)
765        info_item = QtGui.QStandardItem("Info")
766        data_item = QtGui.QStandardItem()
767        data_item.setData(item_from.child(0).data())
768        new_item.setText(item_from.text())
769        new_item.setChild(0, data_item)
770        new_item.setChild(1, info_item)
771        # Append a "unique" descriptor to the name
772        time_bit = str(time.time())[7:-1].replace('.', '')
773        new_name = new_item.text() + '_@' + time_bit
774        new_item.setText(new_name)
775        # Change the underlying data so it is no longer a theory
776        try:
777            new_item.child(0).data().is_data = True
778            new_item.child(0).data().symbol = 'Circle'
779        except AttributeError:
780            #no data here, pass
781            pass
782        return new_item
783
784    def recursivelyCloneItem(self, item):
785        """
786        Clone QStandardItem() object
787        """
788        new_item = item.clone()
789        # clone doesn't do deepcopy :(
790        for child_index in range(item.rowCount()):
791            child_item = self.recursivelyCloneItem(item.child(child_index))
792            new_item.setChild(child_index, child_item)
793        return new_item
794
795    def updatePlotName(self, name_tuple):
796        """
797        Modify the name of the current plot
798        """
799        old_name, current_name = name_tuple
800        ind = self.cbgraph.findText(old_name)
801        self.cbgraph.setCurrentIndex(ind)
802        self.cbgraph.setItemText(ind, current_name)
803
804    def add_data(self, data_list):
805        """
806        Update the data manager with new items
807        """
808        self.manager.add_data(data_list)
809
810    def updateGraphCount(self, graph_list):
811        """
812        Modify the graph name combo and potentially remove
813        deleted graphs
814        """
815        self.updateGraphCombo(graph_list)
816
817        if not self.active_plots:
818            return
819        new_plots = [PlotHelper.plotById(plot) for plot in graph_list]
820        active_plots_copy = list(self.active_plots.keys())
821        for plot in active_plots_copy:
822            if self.active_plots[plot] in new_plots:
823                continue
824            self.active_plots.pop(plot)
825
826    def updateGraphCombo(self, graph_list):
827        """
828        Modify Graph combo box on graph add/delete
829        """
830        orig_text = self.cbgraph.currentText()
831        self.cbgraph.clear()
832        self.cbgraph.insertItems(0, graph_list)
833        ind = self.cbgraph.findText(orig_text)
834        if ind > 0:
835            self.cbgraph.setCurrentIndex(ind)
836
837    def updatePerspectiveCombo(self, index):
838        """
839        Notify the gui manager about the new perspective chosen.
840        """
841        self.communicator.perspectiveChangedSignal.emit(self.cbFitting.itemText(index))
842        self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
843
844    def itemFromFilename(self, filename):
845        """
846        Retrieves model item corresponding to the given filename
847        """
848        item = GuiUtils.itemFromFilename(filename, self.model)
849        return item
850
851    def displayFile(self, filename=None, is_data=True, id=None):
852        """
853        Forces display of charts for the given filename
854        """
855        model = self.model if is_data else self.theory_model
856        # Now query the model item for available plots
857        plots = GuiUtils.plotsFromFilename(filename, model)
858        # Each fitpage contains the name based on fit widget number
859        fitpage_name = "" if id is None else "M"+str(id)
860        new_plots = []
861        for item, plot in plots.items():
862            if self.updatePlot(plot):
863                # Don't create plots which are already displayed
864                continue
865            # Don't plot intermediate results, e.g. P(Q), S(Q)
866            match = GuiUtils.theory_plot_ID_pattern.match(plot.id)
867            # 2nd match group contains the identifier for the intermediate
868            # result, if present (e.g. "[P(Q)]")
869            if match and match.groups()[1] != None:
870                continue
871            # Don't include plots from different fitpages,
872            # but always include the original data
873            if (fitpage_name in plot.name
874                    or filename in plot.name
875                    or filename == plot.filename):
876                # Residuals get their own plot
877                if plot.plot_role == Data1D.ROLE_RESIDUAL:
878                    plot.yscale='linear'
879                    self.plotData([(item, plot)])
880                else:
881                    new_plots.append((item, plot))
882
883        if new_plots:
884            self.plotData(new_plots)
885
886    def displayData(self, data_list, id=None):
887        """
888        Forces display of charts for the given data set
889        """
890        # data_list = [QStandardItem, Data1D/Data2D]
891        plot_to_show = data_list[1]
892        plot_item = data_list[0]
893
894        # plots to show
895        new_plots = []
896
897        # Get the main data plot
898        main_data = GuiUtils.dataFromItem(plot_item.parent())
899        if main_data is None:
900            # Try the current item
901            main_data = GuiUtils.dataFromItem(plot_item)
902
903        # Make sure main data for 2D is always displayed
904        if main_data and not self.isPlotShown(main_data):
905            if isinstance(main_data, Data2D):
906                self.plotData([(plot_item, main_data)])
907
908        # Check if this is merely a plot update
909        if self.updatePlot(plot_to_show):
910            return
911
912        # Residuals get their own plot
913        if plot_to_show.plot_role == Data1D.ROLE_RESIDUAL:
914            plot_to_show.yscale='linear'
915            self.plotData([(plot_item, plot_to_show)])
916        elif plot_to_show.plot_role == Data1D.ROLE_DELETABLE:
917            # No plot
918            return
919        else:
920            # Plots with main data points on the same chart
921            # Get the main data plot
922            if main_data and not self.isPlotShown(main_data):
923                new_plots.append((plot_item, main_data))
924            new_plots.append((plot_item, plot_to_show))
925
926        if new_plots:
927            self.plotData(new_plots)
928
929    def isPlotShown(self, plot):
930        """
931        Checks currently shown plots and returns true if match
932        """
933        if not hasattr(plot, 'name'):
934            return False
935        ids_vals = [val.data.name for val in self.active_plots.values()]
936
937        return plot.name in ids_vals
938
939    def addDataPlot2D(self, plot_set, item):
940        """
941        Create a new 2D plot and add it to the workspace
942        """
943        plot2D = Plotter2D(self)
944        plot2D.item = item
945        plot2D.plot(plot_set)
946        self.addPlot(plot2D)
947        self.active_plots[plot2D.data.name] = plot2D
948        #============================================
949        # Experimental hook for silx charts
950        #============================================
951        ## Attach silx
952        #from silx.gui import qt
953        #from silx.gui.plot import StackView
954        #sv = StackView()
955        #sv.setColormap("jet", autoscale=True)
956        #sv.setStack(plot_set.data.reshape(1,100,100))
957        ##sv.setLabels(["x: -10 to 10 (200 samples)",
958        ##              "y: -10 to 5 (150 samples)"])
959        #sv.show()
960        #============================================
961
962    def plotData(self, plots, transform=True):
963        """
964        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
965        """
966        # Call show on requested plots
967        # All same-type charts in one plot
968        for item, plot_set in plots:
969            if isinstance(plot_set, Data1D):
970                if not 'new_plot' in locals():
971                    new_plot = Plotter(self)
972                    new_plot.item = item
973                new_plot.plot(plot_set, transform=transform)
974                # active_plots may contain multiple charts
975                self.active_plots[plot_set.name] = new_plot
976            elif isinstance(plot_set, Data2D):
977                self.addDataPlot2D(plot_set, item)
978            else:
979                msg = "Incorrect data type passed to Plotting"
980                raise AttributeError(msg)
981
982        if 'new_plot' in locals() and \
983            hasattr(new_plot, 'data') and \
984            isinstance(new_plot.data, Data1D):
985                self.addPlot(new_plot)
986
987    def newPlot(self):
988        """
989        Select checked data and plot it
990        """
991        # Check which tab is currently active
992        if self.current_view == self.treeView:
993            plots = GuiUtils.plotsFromCheckedItems(self.model)
994        else:
995            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
996
997        self.plotData(plots)
998
999    def addPlot(self, new_plot):
1000        """
1001        Helper method for plot bookkeeping
1002        """
1003        # Update the global plot counter
1004        title = str(PlotHelper.idOfPlot(new_plot))
1005        new_plot.setWindowTitle(title)
1006
1007        # Set the object name to satisfy the Squish object picker
1008        new_plot.setObjectName(title)
1009
1010        # Add the plot to the workspace
1011        plot_widget = self.parent.workspace().addSubWindow(new_plot)
1012
1013        # Show the plot
1014        new_plot.show()
1015        new_plot.canvas.draw()
1016
1017        # Update the plot widgets dict
1018        self.plot_widgets[title]=plot_widget
1019
1020        # Update the active chart list
1021        self.active_plots[new_plot.data.name] = new_plot
1022
1023    def appendPlot(self):
1024        """
1025        Add data set(s) to the existing matplotlib chart
1026        """
1027        # new plot data; check which tab is currently active
1028        if self.current_view == self.treeView:
1029            new_plots = GuiUtils.plotsFromCheckedItems(self.model)
1030        else:
1031            new_plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
1032
1033        # old plot data
1034        plot_id = str(self.cbgraph.currentText())
1035        try:
1036            assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
1037        except:
1038            return
1039
1040        old_plot = PlotHelper.plotById(plot_id)
1041
1042        # Add new data to the old plot, if data type is the same.
1043        for _, plot_set in new_plots:
1044            if type(plot_set) is type(old_plot._data):
1045                old_plot.data = plot_set
1046                old_plot.plot()
1047                # need this for lookup - otherwise this plot will never update
1048                self.active_plots[plot_set.name] = old_plot
1049
1050    def updatePlot(self, data):
1051        """
1052        Modify existing plot for immediate response and returns True.
1053        Returns false, if the plot does not exist already.
1054        """
1055        try: # there might be a list or a single value being passed
1056            data = data[0]
1057        except TypeError:
1058            pass
1059        assert type(data).__name__ in ['Data1D', 'Data2D']
1060
1061        ids_keys = list(self.active_plots.keys())
1062        ids_vals = [val.data.name for val in self.active_plots.values()]
1063
1064        data_id = data.name
1065        if data_id in ids_keys:
1066            # We have data, let's replace data that needs replacing
1067            if data.plot_role != Data1D.ROLE_DATA:
1068                self.active_plots[data_id].replacePlot(data_id, data)
1069            return True
1070        elif data_id in ids_vals:
1071            if data.plot_role != Data1D.ROLE_DATA:
1072                list(self.active_plots.values())[ids_vals.index(data_id)].replacePlot(data_id, data)
1073            return True
1074        return False
1075
1076    def chooseFiles(self):
1077        """
1078        Shows the Open file dialog and returns the chosen path(s)
1079        """
1080        # List of known extensions
1081        wlist = self.getWlist()
1082
1083        # Location is automatically saved - no need to keep track of the last dir
1084        # But only with Qt built-in dialog (non-platform native)
1085        paths = QtWidgets.QFileDialog.getOpenFileNames(self, "Choose a file", "",
1086                wlist, None, QtWidgets.QFileDialog.DontUseNativeDialog)[0]
1087        if not paths:
1088            return
1089
1090        if not isinstance(paths, list):
1091            paths = [paths]
1092
1093        return paths
1094
1095    def readData(self, path):
1096        """
1097        verbatim copy-paste from
1098           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
1099        slightly modified for clarity
1100        """
1101        message = ""
1102        log_msg = ''
1103        output = {}
1104        any_error = False
1105        data_error = False
1106        error_message = ""
1107        number_of_files = len(path)
1108        self.communicator.progressBarUpdateSignal.emit(0.0)
1109
1110        for index, p_file in enumerate(path):
1111            basename = os.path.basename(p_file)
1112            _, extension = os.path.splitext(basename)
1113            if extension.lower() in GuiUtils.EXTENSIONS:
1114                any_error = True
1115                log_msg = "Data Loader cannot "
1116                log_msg += "load: %s\n" % str(p_file)
1117                log_msg += """Please try to open that file from "open project" """
1118                log_msg += """or "open analysis" menu\n"""
1119                error_message = log_msg + "\n"
1120                logging.info(log_msg)
1121                continue
1122
1123            try:
1124                message = "Loading Data... " + str(basename) + "\n"
1125
1126                # change this to signal notification in GuiManager
1127                self.communicator.statusBarUpdateSignal.emit(message)
1128
1129                output_objects = self.loader.load(p_file)
1130
1131                # Some loaders return a list and some just a single Data1D object.
1132                # Standardize.
1133                if not isinstance(output_objects, list):
1134                    output_objects = [output_objects]
1135
1136                for item in output_objects:
1137                    # cast sascalc.dataloader.data_info.Data1D into
1138                    # sasgui.guiframe.dataFitting.Data1D
1139                    # TODO : Fix it
1140                    new_data = self.manager.create_gui_data(item, p_file)
1141                    output[new_data.id] = new_data
1142
1143                    # Model update should be protected
1144                    self.mutex.lock()
1145                    self.updateModel(new_data, p_file)
1146                    #self.model.reset()
1147                    QtWidgets.QApplication.processEvents()
1148                    self.mutex.unlock()
1149
1150                    if hasattr(item, 'errors'):
1151                        for error_data in item.errors:
1152                            data_error = True
1153                            message += "\tError: {0}\n".format(error_data)
1154                    else:
1155
1156                        logging.error("Loader returned an invalid object:\n %s" % str(item))
1157                        data_error = True
1158
1159            except Exception as ex:
1160                logging.error(sys.exc_info()[1])
1161
1162                any_error = True
1163            if any_error or error_message != "":
1164                if error_message == "":
1165                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
1166                    error += "while loading Data: \n%s\n" % str(basename)
1167                    error_message += "The data file you selected could not be loaded.\n"
1168                    error_message += "Make sure the content of your file"
1169                    error_message += " is properly formatted.\n\n"
1170                    error_message += "When contacting the SasView team, mention the"
1171                    error_message += " following:\n%s" % str(error)
1172                elif data_error:
1173                    base_message = "Errors occurred while loading "
1174                    base_message += "{0}\n".format(basename)
1175                    base_message += "The data file loaded but with errors.\n"
1176                    error_message = base_message + error_message
1177                else:
1178                    error_message += "%s\n" % str(p_file)
1179
1180            current_percentage = int(100.0* index/number_of_files)
1181            self.communicator.progressBarUpdateSignal.emit(current_percentage)
1182
1183        if any_error or error_message:
1184            logging.error(error_message)
1185            status_bar_message = "Errors occurred while loading %s" % format(basename)
1186            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
1187
1188        else:
1189            message = "Loading Data Complete! "
1190        message += log_msg
1191        # Notify the progress bar that the updates are over.
1192        self.communicator.progressBarUpdateSignal.emit(-1)
1193        self.communicator.statusBarUpdateSignal.emit(message)
1194
1195        return output, message
1196
1197    def getWlist(self):
1198        """
1199        Wildcards of files we know the format of.
1200        """
1201        # Display the Qt Load File module
1202        cards = self.loader.get_wildcards()
1203
1204        # get rid of the wx remnant in wildcards
1205        # TODO: modify sasview loader get_wildcards method, after merge,
1206        # so this kludge can be avoided
1207        new_cards = []
1208        for item in cards:
1209            new_cards.append(item[:item.find("|")])
1210        wlist = ';;'.join(new_cards)
1211
1212        return wlist
1213
1214    def setItemsCheckability(self, model, dimension=None, checked=False):
1215        """
1216        For a given model, check or uncheck all items of given dimension
1217        """
1218        mode = QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked
1219
1220        assert isinstance(checked, bool)
1221
1222        types = (None, Data1D, Data2D)
1223        if not dimension in types:
1224            return
1225
1226        for index in range(model.rowCount()):
1227            item = model.item(index)
1228            if item.isCheckable() and item.checkState() != mode:
1229                data = item.child(0).data()
1230                if dimension is None or isinstance(data, dimension):
1231                    item.setCheckState(mode)
1232
1233            items = list(GuiUtils.getChildrenFromItem(item))
1234
1235            for it in items:
1236                if it.isCheckable() and it.checkState() != mode:
1237                    data = it.child(0).data()
1238                    if dimension is None or isinstance(data, dimension):
1239                        it.setCheckState(mode)
1240
1241    def selectData(self, index):
1242        """
1243        Callback method for modifying the TreeView on Selection Options change
1244        """
1245        if not isinstance(index, int):
1246            msg = "Incorrect type passed to DataExplorer.selectData()"
1247            raise AttributeError(msg)
1248
1249        # Respond appropriately
1250        if index == 0:
1251            self.setItemsCheckability(self.model, checked=True)
1252
1253        elif index == 1:
1254            # De-select All
1255            self.setItemsCheckability(self.model, checked=False)
1256
1257        elif index == 2:
1258            # Select All 1-D
1259            self.setItemsCheckability(self.model, dimension=Data1D, checked=True)
1260
1261        elif index == 3:
1262            # Unselect All 1-D
1263            self.setItemsCheckability(self.model, dimension=Data1D, checked=False)
1264
1265        elif index == 4:
1266            # Select All 2-D
1267            self.setItemsCheckability(self.model, dimension=Data2D, checked=True)
1268
1269        elif index == 5:
1270            # Unselect All 2-D
1271            self.setItemsCheckability(self.model, dimension=Data2D, checked=False)
1272
1273        else:
1274            msg = "Incorrect value in the Selection Option"
1275            # Change this to a proper logging action
1276            raise Exception(msg)
1277
1278    def contextMenu(self):
1279        """
1280        Define actions and layout of the right click context menu
1281        """
1282        # Create a custom menu based on actions defined in the UI file
1283        self.context_menu = QtWidgets.QMenu(self)
1284        self.context_menu.addAction(self.actionDataInfo)
1285        self.context_menu.addAction(self.actionSaveAs)
1286        self.context_menu.addAction(self.actionQuickPlot)
1287        self.context_menu.addSeparator()
1288        self.context_menu.addAction(self.actionQuick3DPlot)
1289        self.context_menu.addAction(self.actionEditMask)
1290        self.context_menu.addSeparator()
1291        self.context_menu.addAction(self.actionFreezeResults)
1292        self.context_menu.addSeparator()
1293        self.context_menu.addAction(self.actionDelete)
1294
1295
1296        # Define the callbacks
1297        self.actionDataInfo.triggered.connect(self.showDataInfo)
1298        self.actionSaveAs.triggered.connect(self.saveDataAs)
1299        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
1300        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
1301        self.actionEditMask.triggered.connect(self.showEditDataMask)
1302        self.actionDelete.triggered.connect(self.deleteSelectedItem)
1303        self.actionFreezeResults.triggered.connect(self.freezeSelectedItems)
1304
1305    def onCustomContextMenu(self, position):
1306        """
1307        Show the right-click context menu in the data treeview
1308        """
1309        index = self.current_view.indexAt(position)
1310        proxy = self.current_view.model()
1311        model = proxy.sourceModel()
1312
1313        if not index.isValid():
1314            return
1315        model_item = model.itemFromIndex(proxy.mapToSource(index))
1316        # Find the mapped index
1317        orig_index = model_item.isCheckable()
1318        if not orig_index:
1319            return
1320        # Check the data to enable/disable actions
1321        is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
1322        self.actionQuick3DPlot.setEnabled(is_2D)
1323        self.actionEditMask.setEnabled(is_2D)
1324
1325        # Freezing
1326        # check that the selection has inner items
1327        freeze_enabled = False
1328        if model_item.parent() is not None:
1329            freeze_enabled = True
1330        self.actionFreezeResults.setEnabled(freeze_enabled)
1331
1332        # Fire up the menu
1333        self.context_menu.exec_(self.current_view.mapToGlobal(position))
1334
1335    def showDataInfo(self):
1336        """
1337        Show a simple read-only text edit with data information.
1338        """
1339        index = self.current_view.selectedIndexes()[0]
1340        proxy = self.current_view.model()
1341        model = proxy.sourceModel()
1342        model_item = model.itemFromIndex(proxy.mapToSource(index))
1343
1344        data = GuiUtils.dataFromItem(model_item)
1345        if isinstance(data, Data1D):
1346            text_to_show = GuiUtils.retrieveData1d(data)
1347            # Hardcoded sizes to enable full width rendering with default font
1348            self.txt_widget.resize(420,600)
1349        else:
1350            text_to_show = GuiUtils.retrieveData2d(data)
1351            # Hardcoded sizes to enable full width rendering with default font
1352            self.txt_widget.resize(700,600)
1353
1354        self.txt_widget.setReadOnly(True)
1355        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
1356        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
1357        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
1358        self.txt_widget.clear()
1359        self.txt_widget.insertPlainText(text_to_show)
1360
1361        self.txt_widget.show()
1362        # Move the slider all the way up, if present
1363        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
1364        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
1365
1366    def saveDataAs(self):
1367        """
1368        Save the data points as either txt or xml
1369        """
1370        index = self.current_view.selectedIndexes()[0]
1371        proxy = self.current_view.model()
1372        model = proxy.sourceModel()
1373        model_item = model.itemFromIndex(proxy.mapToSource(index))
1374
1375        data = GuiUtils.dataFromItem(model_item)
1376        if isinstance(data, Data1D):
1377            GuiUtils.saveData1D(data)
1378        else:
1379            GuiUtils.saveData2D(data)
1380
1381    def quickDataPlot(self):
1382        """
1383        Frozen plot - display an image of the plot
1384        """
1385        index = self.current_view.selectedIndexes()[0]
1386        proxy = self.current_view.model()
1387        model = proxy.sourceModel()
1388        model_item = model.itemFromIndex(proxy.mapToSource(index))
1389
1390        data = GuiUtils.dataFromItem(model_item)
1391
1392        method_name = 'Plotter'
1393        if isinstance(data, Data2D):
1394            method_name='Plotter2D'
1395
1396        self.new_plot = globals()[method_name](self, quickplot=True)
1397        self.new_plot.data = data
1398        #new_plot.plot(marker='o')
1399        self.new_plot.plot()
1400
1401        # Update the global plot counter
1402        title = "Plot " + data.name
1403        self.new_plot.setWindowTitle(title)
1404
1405        # Show the plot
1406        self.new_plot.show()
1407
1408    def quickData3DPlot(self):
1409        """
1410        Slowish 3D plot
1411        """
1412        index = self.current_view.selectedIndexes()[0]
1413        proxy = self.current_view.model()
1414        model = proxy.sourceModel()
1415        model_item = model.itemFromIndex(proxy.mapToSource(index))
1416
1417        data = GuiUtils.dataFromItem(model_item)
1418
1419        self.new_plot = Plotter2D(self, quickplot=True, dimension=3)
1420        self.new_plot.data = data
1421        self.new_plot.plot()
1422
1423        # Update the global plot counter
1424        title = "Plot " + data.name
1425        self.new_plot.setWindowTitle(title)
1426
1427        # Show the plot
1428        self.new_plot.show()
1429
1430    def extShowEditDataMask(self):
1431        self.showEditDataMask()
1432
1433    def showEditDataMask(self, data=None):
1434        """
1435        Mask Editor for 2D plots
1436        """
1437        msg = QtWidgets.QMessageBox()
1438        msg.setIcon(QtWidgets.QMessageBox.Information)
1439        msg.setText("Error: cannot apply mask.\n"+
1440                    "Please select a 2D dataset.")
1441        msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
1442
1443        try:
1444            if data is None or not isinstance(data, Data2D):
1445                # if data wasn't passed - try to get it from
1446                # the currently selected item
1447                index = self.current_view.selectedIndexes()[0]
1448                proxy = self.current_view.model()
1449                model = proxy.sourceModel()
1450                model_item = model.itemFromIndex(proxy.mapToSource(index))
1451
1452                data = GuiUtils.dataFromItem(model_item)
1453
1454            if data is None or not isinstance(data, Data2D):
1455                # If data is still not right, complain
1456                msg.exec_()
1457                return
1458        except:
1459            msg.exec_()
1460            return
1461
1462        mask_editor = MaskEditor(self, data)
1463        # Modal dialog here.
1464        mask_editor.exec_()
1465
1466    def freezeItem(self, item=None):
1467        """
1468        Freeze given item
1469        """
1470        if item is None:
1471            return
1472        self.model.beginResetModel()
1473        new_item = self.cloneTheory(item)
1474        self.model.appendRow(new_item)
1475        self.model.endResetModel()
1476
1477    def freezeDataToItem(self, data=None):
1478        """
1479        Freeze given set of data to main model
1480        """
1481        if data is None:
1482            return
1483        self.model.beginResetModel()
1484        # Append a "unique" descriptor to the name
1485        time_bit = str(time.time())[7:-1].replace('.', '')
1486        new_name = data.name + '_@' + time_bit
1487        # Change the underlying data so it is no longer a theory
1488        try:
1489            data.is_data = True
1490            data.symbol = 'Circle'
1491        except AttributeError:
1492            #no data here, pass
1493            pass
1494        new_item = GuiUtils.createModelItemWithPlot(data, new_name)
1495
1496        self.model.appendRow(new_item)
1497        self.model.endResetModel()
1498
1499    def freezeSelectedItems(self):
1500        """
1501        Freeze selected items
1502        """
1503        indices = self.treeView.selectedIndexes()
1504
1505        proxy = self.treeView.model()
1506        model = proxy.sourceModel()
1507
1508        for index in indices:
1509            row_index = proxy.mapToSource(index)
1510            item_to_copy = model.itemFromIndex(row_index)
1511            if item_to_copy and item_to_copy.isCheckable():
1512                self.freezeItem(item_to_copy)
1513
1514    def deleteAllItems(self):
1515        """
1516        Deletes all datasets from both model and theory_model
1517        """
1518        deleted_items = [self.model.item(row) for row in range(self.model.rowCount())
1519                         if self.model.item(row).isCheckable()]
1520        deleted_names = [item.text() for item in deleted_items]
1521        # Let others know we deleted data
1522        self.communicator.dataDeletedSignal.emit(deleted_items)
1523        # update stored_data
1524        self.manager.update_stored_data(deleted_names)
1525
1526        # Clear the model
1527        self.model.clear()
1528
1529    def deleteSelectedItem(self):
1530        """
1531        Delete the current item
1532        """
1533        # Assure this is indeed wanted
1534        delete_msg = "This operation will delete the selected data sets " +\
1535                     "and all the dependents." +\
1536                     "\nDo you want to continue?"
1537        reply = QtWidgets.QMessageBox.question(self,
1538                                           'Warning',
1539                                           delete_msg,
1540                                           QtWidgets.QMessageBox.Yes,
1541                                           QtWidgets.QMessageBox.No)
1542
1543        if reply == QtWidgets.QMessageBox.No:
1544            return
1545
1546        indices = self.current_view.selectedIndexes()
1547        self.deleteIndices(indices)
1548
1549    def deleteIndices(self, indices):
1550        """
1551        Delete model idices from the current view
1552        """
1553        proxy = self.current_view.model()
1554        model = proxy.sourceModel()
1555
1556        deleted_items = []
1557        deleted_names = []
1558
1559        # Every time a row is removed, the indices change, so we'll just remove
1560        # rows and keep calling selectedIndexes until it returns an empty list.
1561        while len(indices) > 0:
1562            index = indices[0]
1563            #row_index = proxy.mapToSource(index)
1564            #item_to_delete = model.itemFromIndex(row_index)
1565            item_to_delete = model.itemFromIndex(index)
1566            if item_to_delete and item_to_delete.isCheckable():
1567                #row = row_index.row()
1568                row = index.row()
1569
1570                # store the deleted item details so we can pass them on later
1571                deleted_names.append(item_to_delete.text())
1572                deleted_items.append(item_to_delete)
1573
1574                # Delete corresponding open plots
1575                self.closePlotsForItem(item_to_delete)
1576
1577                if item_to_delete.parent():
1578                    # We have a child item - delete from it
1579                    item_to_delete.parent().removeRow(row)
1580                else:
1581                    # delete directly from model
1582                    model.removeRow(row)
1583            indices = self.current_view.selectedIndexes()
1584
1585        # Let others know we deleted data
1586        self.communicator.dataDeletedSignal.emit(deleted_items)
1587
1588        # update stored_data
1589        self.manager.update_stored_data(deleted_names)
1590
1591    def closeAllPlots(self):
1592        """
1593        Close all currently displayed plots
1594        """
1595
1596        for plot_id in PlotHelper.currentPlots():
1597            try:
1598                plotter = PlotHelper.plotById(plot_id)
1599                plotter.close()
1600                self.plot_widgets[plot_id].close()
1601                self.plot_widgets.pop(plot_id, None)
1602            except AttributeError as ex:
1603                logging.error("Closing of %s failed:\n %s" % (plot_id, str(ex)))
1604
1605    def closePlotsForItem(self, item):
1606        """
1607        Given standard item, close all its currently displayed plots
1608        """
1609        # item - HashableStandardItems of active plots
1610
1611        # {} -> 'Graph1' : HashableStandardItem()
1612        current_plot_items = {}
1613        for plot_name in PlotHelper.currentPlots():
1614            current_plot_items[plot_name] = PlotHelper.plotById(plot_name).item
1615
1616        # item and its hashable children
1617        items_being_deleted = []
1618        if item.rowCount() > 0:
1619            items_being_deleted = [item.child(n) for n in range(item.rowCount())
1620                                   if isinstance(item.child(n), GuiUtils.HashableStandardItem)]
1621        items_being_deleted.append(item)
1622        # Add the parent in case a child is selected
1623        if isinstance(item.parent(), GuiUtils.HashableStandardItem):
1624            items_being_deleted.append(item.parent())
1625
1626        # Compare plot items and items to delete
1627        plots_to_close = set(current_plot_items.values()) & set(items_being_deleted)
1628
1629        for plot_item in plots_to_close:
1630            for plot_name in current_plot_items.keys():
1631                if plot_item == current_plot_items[plot_name]:
1632                    plotter = PlotHelper.plotById(plot_name)
1633                    # try to delete the plot
1634                    try:
1635                        plotter.close()
1636                        #self.parent.workspace().removeSubWindow(plotter)
1637                        self.plot_widgets[plot_name].close()
1638                        self.plot_widgets.pop(plot_name, None)
1639                    except AttributeError as ex:
1640                        logging.error("Closing of %s failed:\n %s" % (plot_name, str(ex)))
1641
1642        pass # debugger anchor
1643
1644    def onAnalysisUpdate(self, new_perspective=""):
1645        """
1646        Update the perspective combo index based on passed string
1647        """
1648        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1649        self.cbFitting.blockSignals(True)
1650        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1651        self.cbFitting.blockSignals(False)
1652        pass
1653
1654    def loadComplete(self, output):
1655        """
1656        Post message to status bar and update the data manager
1657        """
1658        assert isinstance(output, tuple)
1659
1660        # Reset the model so the view gets updated.
1661        #self.model.reset()
1662        self.communicator.progressBarUpdateSignal.emit(-1)
1663
1664        output_data = output[0]
1665        message = output[1]
1666        # Notify the manager of the new data available
1667        self.communicator.statusBarUpdateSignal.emit(message)
1668        self.communicator.fileDataReceivedSignal.emit(output_data)
1669        self.manager.add_data(data_list=output_data)
1670
1671    def loadFailed(self, reason):
1672        print("File Load Failed with:\n", reason)
1673        pass
1674
1675    def updateModel(self, data, p_file):
1676        """
1677        Add data and Info fields to the model item
1678        """
1679        # Structure of the model
1680        # checkbox + basename
1681        #     |-------> Data.D object
1682        #     |-------> Info
1683        #                 |----> Title:
1684        #                 |----> Run:
1685        #                 |----> Type:
1686        #                 |----> Path:
1687        #                 |----> Process
1688        #                          |-----> process[0].name
1689        #     |-------> THEORIES
1690
1691        # Top-level item: checkbox with label
1692        checkbox_item = GuiUtils.HashableStandardItem()
1693        checkbox_item.setCheckable(True)
1694        checkbox_item.setCheckState(QtCore.Qt.Checked)
1695        if p_file is not None:
1696            checkbox_item.setText(os.path.basename(p_file))
1697
1698        # Add the actual Data1D/Data2D object
1699        object_item = GuiUtils.HashableStandardItem()
1700        object_item.setData(data)
1701
1702        checkbox_item.setChild(0, object_item)
1703
1704        # Add rows for display in the view
1705        info_item = GuiUtils.infoFromData(data)
1706
1707        # Set info_item as the first child
1708        checkbox_item.setChild(1, info_item)
1709
1710        # Caption for the theories
1711        checkbox_item.setChild(2, QtGui.QStandardItem("FIT RESULTS"))
1712
1713        # New row in the model
1714        self.model.beginResetModel()
1715        self.model.appendRow(checkbox_item)
1716        self.model.endResetModel()
1717
1718    def updateModelFromPerspective(self, model_item):
1719        """
1720        Receive an update model item from a perspective
1721        Make sure it is valid and if so, replace it in the model
1722        """
1723        # Assert the correct type
1724        if not isinstance(model_item, QtGui.QStandardItem):
1725            msg = "Wrong data type returned from calculations."
1726            raise AttributeError(msg)
1727
1728        # TODO: Assert other properties
1729
1730        # Reset the view
1731        ##self.model.reset()
1732        # Pass acting as a debugger anchor
1733        pass
1734
1735    def updateTheoryFromPerspective(self, model_item):
1736        """
1737        Receive an update theory item from a perspective
1738        Make sure it is valid and if so, replace/add in the model
1739        """
1740        # Assert the correct type
1741        if not isinstance(model_item, QtGui.QStandardItem):
1742            msg = "Wrong data type returned from calculations."
1743            raise AttributeError(msg)
1744
1745        # Check if there are any other items for this tab
1746        # If so, delete them
1747        current_tab_name = model_item.text()
1748        for current_index in range(self.theory_model.rowCount()):
1749            if current_tab_name == self.theory_model.item(current_index).text():
1750                self.theory_model.removeRow(current_index)
1751                break
1752        # send in the new item
1753        self.theory_model.appendRow(model_item)
1754
1755    def deleteIntermediateTheoryPlotsByModelID(self, model_id):
1756        """Given a model's ID, deletes all items in the theory item model which reference the same ID. Useful in the
1757        case of intermediate results disappearing when changing calculations (in which case you don't want them to be
1758        retained in the list)."""
1759        items_to_delete = []
1760        for r in range(self.theory_model.rowCount()):
1761            item = self.theory_model.item(r, 0)
1762            data = item.child(0).data()
1763            if not hasattr(data, "id"):
1764                return
1765            match = GuiUtils.theory_plot_ID_pattern.match(data.id)
1766            if match:
1767                item_model_id = match.groups()[-1]
1768                if item_model_id == model_id:
1769                    # Only delete those identified as an intermediate plot
1770                    if match.groups()[2] not in (None, ""):
1771                        items_to_delete.append(item)
1772
1773        for item in items_to_delete:
1774            self.theory_model.removeRow(item.row())
Note: See TracBrowser for help on using the repository browser.