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

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

Finalize backward compatibility mode for project read for simple fitting

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