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

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

Merge branch 'ESS_GUI' into ESS_GUI_project_save

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