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

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

Complain when wrong data sent to perspective. SASVIEW-1165 SASVIEW-1166

  • Property mode set to 100644
File size: 52.1 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.currentIndexChanged.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        kwargs = {
228            'parent'    : self,
229            'caption'   : 'Open Project',
230            'filter'    : 'Project (*.json);;All files (*.*)',
231            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
232        }
233        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
234        if filename:
235            load_thread = threads.deferToThread(self.readProject, filename)
236            load_thread.addCallback(self.readProjectComplete)
237            load_thread.addErrback(self.readProjectFailed)
238
239    def loadFailed(self, reason):
240        """
241        """
242        print("file load FAILED: ", reason)
243        pass
244
245    def readProjectFailed(self, reason):
246        """
247        """
248        print("readProjectFailed FAILED: ", reason)
249        pass
250
251    def readProject(self, filename):
252        self.communicator.statusBarUpdateSignal.emit("Loading Project... %s" % os.path.basename(filename))
253        try:
254            manager = DataManager()
255            with open(filename, 'r') as infile:
256                manager.load_from_readable(infile)
257
258            self.communicator.statusBarUpdateSignal.emit("Loaded Project: %s" % os.path.basename(filename))
259            return manager
260
261        except:
262            self.communicator.statusBarUpdateSignal.emit("Failed: %s" % os.path.basename(filename))
263            raise
264
265    def readProjectComplete(self, manager):
266        self.model.clear()
267
268        self.manager.assign(manager)
269        self.model.beginResetModel()
270        for id, item in self.manager.get_all_data().items():
271            self.updateModel(item.data, item.path)
272
273        self.model.endResetModel()
274
275    def saveProject(self):
276        """
277        Called when the "Save Project" menu item chosen.
278        """
279        kwargs = {
280            'parent'    : self,
281            'caption'   : 'Save Project',
282            'filter'    : 'Project (*.json)',
283            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
284        }
285        name_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
286        filename = name_tuple[0]
287        if filename:
288            _, extension = os.path.splitext(filename)
289            if not extension:
290                filename = '.'.join((filename, 'json'))
291            self.communicator.statusBarUpdateSignal.emit("Saving Project... %s\n" % os.path.basename(filename))
292            with open(filename, 'w') as outfile:
293                self.manager.save_to_writable(outfile)
294
295    def deleteFile(self, event):
296        """
297        Delete selected rows from the model
298        """
299        # Assure this is indeed wanted
300        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
301                     "\nDo you want to continue?"
302        reply = QtWidgets.QMessageBox.question(self,
303                                           'Warning',
304                                           delete_msg,
305                                           QtWidgets.QMessageBox.Yes,
306                                           QtWidgets.QMessageBox.No)
307
308        if reply == QtWidgets.QMessageBox.No:
309            return
310
311        # Figure out which rows are checked
312        ind = -1
313        # Use 'while' so the row count is forced at every iteration
314        deleted_items = []
315        deleted_names = []
316        while ind < self.model.rowCount():
317            ind += 1
318            item = self.model.item(ind)
319
320            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
321                # Delete these rows from the model
322                deleted_names.append(str(self.model.item(ind).text()))
323                deleted_items.append(item)
324
325                self.model.removeRow(ind)
326                # Decrement index since we just deleted it
327                ind -= 1
328
329        # Let others know we deleted data
330        self.communicator.dataDeletedSignal.emit(deleted_items)
331
332        # update stored_data
333        self.manager.update_stored_data(deleted_names)
334
335    def deleteTheory(self, event):
336        """
337        Delete selected rows from the theory model
338        """
339        # Assure this is indeed wanted
340        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
341                     "\nDo you want to continue?"
342        reply = QtWidgets.QMessageBox.question(self,
343                                           'Warning',
344                                           delete_msg,
345                                           QtWidgets.QMessageBox.Yes,
346                                           QtWidgets.QMessageBox.No)
347
348        if reply == QtWidgets.QMessageBox.No:
349            return
350
351        # Figure out which rows are checked
352        ind = -1
353        # Use 'while' so the row count is forced at every iteration
354        while ind < self.theory_model.rowCount():
355            ind += 1
356            item = self.theory_model.item(ind)
357            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
358                # Delete these rows from the model
359                self.theory_model.removeRow(ind)
360                # Decrement index since we just deleted it
361                ind -= 1
362
363        # pass temporarily kept as a breakpoint anchor
364        pass
365
366    def sendData(self, event):
367        """
368        Send selected item data to the current perspective and set the relevant notifiers
369        """
370        # Set the signal handlers
371        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
372
373        def isItemReady(index):
374            item = self.model.item(index)
375            return item.isCheckable() and item.checkState() == QtCore.Qt.Checked
376
377        # Figure out which rows are checked
378        selected_items = [self.model.item(index)
379                          for index in range(self.model.rowCount())
380                          if isItemReady(index)]
381
382        if len(selected_items) < 1:
383            return
384
385        # Which perspective has been selected?
386        if len(selected_items) > 1 and not self._perspective().allowBatch():
387            if hasattr(self._perspective(), 'title'):
388                title = self._perspective().title()
389            else:
390                title = self._perspective().windowTitle()
391            msg = title + " does not allow multiple data."
392            msgbox = QtWidgets.QMessageBox()
393            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
394            msgbox.setText(msg)
395            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
396            retval = msgbox.exec_()
397            return
398
399        # Notify the GuiManager about the send request
400        try:
401            self._perspective().setData(data_item=selected_items, is_batch=self.chkBatch.isChecked())
402        except Exception as ex:
403            msg = "%s perspective returned the following message: \n%s\n" %(self._perspective().name, str(ex))
404            logging.error(msg)
405            msg = str(ex)
406            msgbox = QtWidgets.QMessageBox()
407            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
408            msgbox.setText(msg)
409            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
410            retval = msgbox.exec_()
411
412
413    def freezeCheckedData(self):
414        """
415        Convert checked results (fitted model, residuals) into separate dataset.
416        """
417        outer_index = -1
418        theories_copied = 0
419        orig_model_size = self.model.rowCount()
420        while outer_index < orig_model_size:
421            outer_index += 1
422            outer_item = self.model.item(outer_index)
423            if not outer_item:
424                continue
425            if not outer_item.isCheckable():
426                continue
427            # Look for checked inner items
428            inner_index = -1
429            while inner_index < outer_item.rowCount():
430               inner_item = outer_item.child(inner_index)
431               inner_index += 1
432               if not inner_item:
433                   continue
434               if not inner_item.isCheckable():
435                   continue
436               if inner_item.checkState() != QtCore.Qt.Checked:
437                   continue
438               self.model.beginResetModel()
439               theories_copied += 1
440               new_item = self.cloneTheory(inner_item)
441               self.model.appendRow(new_item)
442               self.model.endResetModel()
443
444        freeze_msg = ""
445        if theories_copied == 0:
446            return
447        elif theories_copied == 1:
448            freeze_msg = "1 theory copied to a separate data set"
449        elif theories_copied > 1:
450            freeze_msg = "%i theories copied to separate data sets" % theories_copied
451        else:
452            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
453            raise AttributeError(freeze_msg)
454        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
455
456    def freezeTheory(self, event):
457        """
458        Freeze selected theory rows.
459
460        "Freezing" means taking the plottable data from the Theory item
461        and copying it to a separate top-level item in Data.
462        """
463        # Figure out which rows are checked
464        # Use 'while' so the row count is forced at every iteration
465        outer_index = -1
466        theories_copied = 0
467        while outer_index < self.theory_model.rowCount():
468            outer_index += 1
469            outer_item = self.theory_model.item(outer_index)
470            if not outer_item:
471                continue
472            if outer_item.isCheckable() and \
473                   outer_item.checkState() == QtCore.Qt.Checked:
474                self.model.beginResetModel()
475                theories_copied += 1
476                new_item = self.cloneTheory(outer_item)
477                self.model.appendRow(new_item)
478                self.model.endResetModel()
479
480        freeze_msg = ""
481        if theories_copied == 0:
482            return
483        elif theories_copied == 1:
484            freeze_msg = "1 theory copied from the Theory tab as a data set"
485        elif theories_copied > 1:
486            freeze_msg = "%i theories copied from the Theory tab as data sets" % theories_copied
487        else:
488            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
489            raise AttributeError(freeze_msg)
490        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
491        # Actively switch tabs
492        self.setCurrentIndex(1)
493
494    def cloneTheory(self, item_from):
495        """
496        Manually clone theory items into a new HashableItem
497        """
498        new_item = GuiUtils.HashableStandardItem()
499        new_item.setCheckable(True)
500        new_item.setCheckState(QtCore.Qt.Checked)
501        info_item = QtGui.QStandardItem("Info")
502        data_item = QtGui.QStandardItem()
503        data_item.setData(item_from.child(0).data())
504        new_item.setText(item_from.text())
505        new_item.setChild(0, data_item)
506        new_item.setChild(1, info_item)
507        # Append a "unique" descriptor to the name
508        time_bit = str(time.time())[7:-1].replace('.', '')
509        new_name = new_item.text() + '_@' + time_bit
510        new_item.setText(new_name)
511        # Change the underlying data so it is no longer a theory
512        try:
513            new_item.child(0).data().is_data = True
514        except AttributeError:
515            #no data here, pass
516            pass
517        return new_item
518
519    def recursivelyCloneItem(self, item):
520        """
521        Clone QStandardItem() object
522        """
523        new_item = item.clone()
524        # clone doesn't do deepcopy :(
525        for child_index in range(item.rowCount()):
526            child_item = self.recursivelyCloneItem(item.child(child_index))
527            new_item.setChild(child_index, child_item)
528        return new_item
529
530    def updatePlotName(self, name_tuple):
531        """
532        Modify the name of the current plot
533        """
534        old_name, current_name = name_tuple
535        ind = self.cbgraph.findText(old_name)
536        self.cbgraph.setCurrentIndex(ind)
537        self.cbgraph.setItemText(ind, current_name)
538
539    def updateGraphCount(self, graph_list):
540        """
541        Modify the graph name combo and potentially remove
542        deleted graphs
543        """
544        self.updateGraphCombo(graph_list)
545
546        if not self.active_plots:
547            return
548        new_plots = [PlotHelper.plotById(plot) for plot in graph_list]
549        active_plots_copy = list(self.active_plots.keys())
550        for plot in active_plots_copy:
551            if self.active_plots[plot] in new_plots:
552                continue
553            self.active_plots.pop(plot)
554
555    def updateGraphCombo(self, graph_list):
556        """
557        Modify Graph combo box on graph add/delete
558        """
559        orig_text = self.cbgraph.currentText()
560        self.cbgraph.clear()
561        self.cbgraph.insertItems(0, graph_list)
562        ind = self.cbgraph.findText(orig_text)
563        if ind > 0:
564            self.cbgraph.setCurrentIndex(ind)
565
566    def updatePerspectiveCombo(self, index):
567        """
568        Notify the gui manager about the new perspective chosen.
569        """
570        self.communicator.perspectiveChangedSignal.emit(self.cbFitting.itemText(index))
571        self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
572
573    def itemFromFilename(self, filename):
574        """
575        Retrieves model item corresponding to the given filename
576        """
577        item = GuiUtils.itemFromFilename(filename, self.model)
578        return item
579
580    def displayFile(self, filename=None, is_data=True, id=None):
581        """
582        Forces display of charts for the given filename
583        """
584        model = self.model if is_data else self.theory_model
585        # Now query the model item for available plots
586        plots = GuiUtils.plotsFromFilename(filename, model)
587        # Each fitpage contains the name based on fit widget number
588        fitpage_name = "" if id is None else "M"+str(id)
589        new_plots = []
590        for item, plot in plots.items():
591            if self.updatePlot(plot):
592                # Don't create plots which are already displayed
593                continue
594            # Don't plot intermediate results, e.g. P(Q), S(Q)
595            match = GuiUtils.theory_plot_ID_pattern.match(plot.id)
596            # 2nd match group contains the identifier for the intermediate
597            # result, if present (e.g. "[P(Q)]")
598            if match and match.groups()[1] != None:
599                continue
600            # Don't include plots from different fitpages,
601            # but always include the original data
602            if (fitpage_name in plot.name
603                    or filename in plot.name
604                    or filename == plot.filename):
605                # Residuals get their own plot
606                if plot.plot_role == Data1D.ROLE_RESIDUAL:
607                    plot.yscale='linear'
608                    self.plotData([(item, plot)])
609                else:
610                    new_plots.append((item, plot))
611
612        if new_plots:
613            self.plotData(new_plots)
614
615    def displayData(self, data_list, id=None):
616        """
617        Forces display of charts for the given data set
618        """
619        # data_list = [QStandardItem, Data1D/Data2D]
620        plot_to_show = data_list[1]
621        plot_item = data_list[0]
622
623        # plots to show
624        new_plots = []
625
626        # Check if this is merely a plot update
627        if self.updatePlot(plot_to_show):
628            return
629
630        # Residuals get their own plot
631        if plot_to_show.plot_role == Data1D.ROLE_RESIDUAL:
632            plot_to_show.yscale='linear'
633            self.plotData([(plot_item, plot_to_show)])
634        elif plot_to_show.plot_role == Data1D.ROLE_DELETABLE:
635            # No plot
636            return
637        else:
638            # Plots with main data points on the same chart
639            # Get the main data plot
640            main_data = GuiUtils.dataFromItem(plot_item.parent())
641            if main_data is None:
642                # Try the current item
643                main_data = GuiUtils.dataFromItem(plot_item)
644            if main_data is not None:
645                new_plots.append((plot_item, main_data))
646            new_plots.append((plot_item, plot_to_show))
647
648        if new_plots:
649            self.plotData(new_plots)
650
651    def addDataPlot2D(self, plot_set, item):
652        """
653        Create a new 2D plot and add it to the workspace
654        """
655        plot2D = Plotter2D(self)
656        plot2D.item = item
657        plot2D.plot(plot_set)
658        self.addPlot(plot2D)
659        self.active_plots[plot2D.data.name] = plot2D
660        #============================================
661        # Experimental hook for silx charts
662        #============================================
663        ## Attach silx
664        #from silx.gui import qt
665        #from silx.gui.plot import StackView
666        #sv = StackView()
667        #sv.setColormap("jet", autoscale=True)
668        #sv.setStack(plot_set.data.reshape(1,100,100))
669        ##sv.setLabels(["x: -10 to 10 (200 samples)",
670        ##              "y: -10 to 5 (150 samples)"])
671        #sv.show()
672        #============================================
673
674    def plotData(self, plots, transform=True):
675        """
676        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
677        """
678        # Call show on requested plots
679        # All same-type charts in one plot
680        for item, plot_set in plots:
681            if isinstance(plot_set, Data1D):
682                if not 'new_plot' in locals():
683                    new_plot = Plotter(self)
684                    new_plot.item = item
685                new_plot.plot(plot_set, transform=transform)
686                # active_plots may contain multiple charts
687                self.active_plots[plot_set.name] = new_plot
688            elif isinstance(plot_set, Data2D):
689                self.addDataPlot2D(plot_set, item)
690            else:
691                msg = "Incorrect data type passed to Plotting"
692                raise AttributeError(msg)
693
694        if 'new_plot' in locals() and \
695            hasattr(new_plot, 'data') and \
696            isinstance(new_plot.data, Data1D):
697                self.addPlot(new_plot)
698
699    def newPlot(self):
700        """
701        Select checked data and plot it
702        """
703        # Check which tab is currently active
704        if self.current_view == self.treeView:
705            plots = GuiUtils.plotsFromCheckedItems(self.model)
706        else:
707            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
708
709        self.plotData(plots)
710
711    def addPlot(self, new_plot):
712        """
713        Helper method for plot bookkeeping
714        """
715        # Update the global plot counter
716        title = str(PlotHelper.idOfPlot(new_plot))
717        new_plot.setWindowTitle(title)
718
719        # Set the object name to satisfy the Squish object picker
720        new_plot.setObjectName(title)
721
722        # Add the plot to the workspace
723        plot_widget = self.parent.workspace().addSubWindow(new_plot)
724
725        # Show the plot
726        new_plot.show()
727        new_plot.canvas.draw()
728
729        # Update the plot widgets dict
730        self.plot_widgets[title]=plot_widget
731
732        # Update the active chart list
733        self.active_plots[new_plot.data.name] = new_plot
734
735    def appendPlot(self):
736        """
737        Add data set(s) to the existing matplotlib chart
738        """
739        # new plot data; check which tab is currently active
740        if self.current_view == self.treeView:
741            new_plots = GuiUtils.plotsFromCheckedItems(self.model)
742        else:
743            new_plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
744
745        # old plot data
746        plot_id = str(self.cbgraph.currentText())
747        try:
748            assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
749        except:
750            return
751
752        old_plot = PlotHelper.plotById(plot_id)
753
754        # Add new data to the old plot, if data type is the same.
755        for _, plot_set in new_plots:
756            if type(plot_set) is type(old_plot._data):
757                old_plot.data = plot_set
758                old_plot.plot()
759                # need this for lookup - otherwise this plot will never update
760                self.active_plots[plot_set.name] = old_plot
761
762    def updatePlot(self, data):
763        """
764        Modify existing plot for immediate response and returns True.
765        Returns false, if the plot does not exist already.
766        """
767        try: # there might be a list or a single value being passed
768            data = data[0]
769        except TypeError:
770            pass
771        assert type(data).__name__ in ['Data1D', 'Data2D']
772
773        ids_keys = list(self.active_plots.keys())
774        ids_vals = [val.data.name for val in self.active_plots.values()]
775
776        data_id = data.name
777        if data_id in ids_keys:
778            # We have data, let's replace data that needs replacing
779            if data.plot_role != Data1D.ROLE_DATA:
780                self.active_plots[data_id].replacePlot(data_id, data)
781            return True
782        elif data_id in ids_vals:
783            if data.plot_role != Data1D.ROLE_DATA:
784                list(self.active_plots.values())[ids_vals.index(data_id)].replacePlot(data_id, data)
785            return True
786        return False
787
788    def chooseFiles(self):
789        """
790        Shows the Open file dialog and returns the chosen path(s)
791        """
792        # List of known extensions
793        wlist = self.getWlist()
794
795        # Location is automatically saved - no need to keep track of the last dir
796        # But only with Qt built-in dialog (non-platform native)
797        paths = QtWidgets.QFileDialog.getOpenFileNames(self, "Choose a file", "",
798                wlist, None, QtWidgets.QFileDialog.DontUseNativeDialog)[0]
799        if not paths:
800            return
801
802        if not isinstance(paths, list):
803            paths = [paths]
804
805        return paths
806
807    def readData(self, path):
808        """
809        verbatim copy-paste from
810           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
811        slightly modified for clarity
812        """
813        message = ""
814        log_msg = ''
815        output = {}
816        any_error = False
817        data_error = False
818        error_message = ""
819        number_of_files = len(path)
820        self.communicator.progressBarUpdateSignal.emit(0.0)
821
822        for index, p_file in enumerate(path):
823            basename = os.path.basename(p_file)
824            _, extension = os.path.splitext(basename)
825            if extension.lower() in GuiUtils.EXTENSIONS:
826                any_error = True
827                log_msg = "Data Loader cannot "
828                log_msg += "load: %s\n" % str(p_file)
829                log_msg += """Please try to open that file from "open project" """
830                log_msg += """or "open analysis" menu\n"""
831                error_message = log_msg + "\n"
832                logging.info(log_msg)
833                continue
834
835            try:
836                message = "Loading Data... " + str(basename) + "\n"
837
838                # change this to signal notification in GuiManager
839                self.communicator.statusBarUpdateSignal.emit(message)
840
841                output_objects = self.loader.load(p_file)
842
843                # Some loaders return a list and some just a single Data1D object.
844                # Standardize.
845                if not isinstance(output_objects, list):
846                    output_objects = [output_objects]
847
848                for item in output_objects:
849                    # cast sascalc.dataloader.data_info.Data1D into
850                    # sasgui.guiframe.dataFitting.Data1D
851                    # TODO : Fix it
852                    new_data = self.manager.create_gui_data(item, p_file)
853                    output[new_data.id] = new_data
854
855                    # Model update should be protected
856                    self.mutex.lock()
857                    self.updateModel(new_data, p_file)
858                    #self.model.reset()
859                    QtWidgets.QApplication.processEvents()
860                    self.mutex.unlock()
861
862                    if hasattr(item, 'errors'):
863                        for error_data in item.errors:
864                            data_error = True
865                            message += "\tError: {0}\n".format(error_data)
866                    else:
867
868                        logging.error("Loader returned an invalid object:\n %s" % str(item))
869                        data_error = True
870
871            except Exception as ex:
872                logging.error(sys.exc_info()[1])
873
874                any_error = True
875            if any_error or error_message != "":
876                if error_message == "":
877                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
878                    error += "while loading Data: \n%s\n" % str(basename)
879                    error_message += "The data file you selected could not be loaded.\n"
880                    error_message += "Make sure the content of your file"
881                    error_message += " is properly formatted.\n\n"
882                    error_message += "When contacting the SasView team, mention the"
883                    error_message += " following:\n%s" % str(error)
884                elif data_error:
885                    base_message = "Errors occurred while loading "
886                    base_message += "{0}\n".format(basename)
887                    base_message += "The data file loaded but with errors.\n"
888                    error_message = base_message + error_message
889                else:
890                    error_message += "%s\n" % str(p_file)
891
892            current_percentage = int(100.0* index/number_of_files)
893            self.communicator.progressBarUpdateSignal.emit(current_percentage)
894
895        if any_error or error_message:
896            logging.error(error_message)
897            status_bar_message = "Errors occurred while loading %s" % format(basename)
898            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
899
900        else:
901            message = "Loading Data Complete! "
902        message += log_msg
903        # Notify the progress bar that the updates are over.
904        self.communicator.progressBarUpdateSignal.emit(-1)
905        self.communicator.statusBarUpdateSignal.emit(message)
906
907        return output, message
908
909    def getWlist(self):
910        """
911        Wildcards of files we know the format of.
912        """
913        # Display the Qt Load File module
914        cards = self.loader.get_wildcards()
915
916        # get rid of the wx remnant in wildcards
917        # TODO: modify sasview loader get_wildcards method, after merge,
918        # so this kludge can be avoided
919        new_cards = []
920        for item in cards:
921            new_cards.append(item[:item.find("|")])
922        wlist = ';;'.join(new_cards)
923
924        return wlist
925
926    def setItemsCheckability(self, model, dimension=None, checked=False):
927        """
928        For a given model, check or uncheck all items of given dimension
929        """
930        mode = QtCore.Qt.Checked if checked else QtCore.Qt.Unchecked
931
932        assert isinstance(checked, bool)
933
934        types = (None, Data1D, Data2D)
935        assert dimension in types
936
937        for index in range(model.rowCount()):
938            item = model.item(index)
939            if dimension is not None and not isinstance(GuiUtils.dataFromItem(item), dimension):
940                continue
941            if item.isCheckable() and item.checkState() != mode:
942                item.setCheckState(mode)
943            # look for all children
944            for inner_index in range(item.rowCount()):
945                child = item.child(inner_index)
946                if child.isCheckable() and child.checkState() != mode:
947                    child.setCheckState(mode)
948
949    def selectData(self, index):
950        """
951        Callback method for modifying the TreeView on Selection Options change
952        """
953        if not isinstance(index, int):
954            msg = "Incorrect type passed to DataExplorer.selectData()"
955            raise AttributeError(msg)
956
957        # Respond appropriately
958        if index == 0:
959            self.setItemsCheckability(self.model, checked=True)
960
961        elif index == 1:
962            # De-select All
963            self.setItemsCheckability(self.model, checked=False)
964
965        elif index == 2:
966            # Select All 1-D
967            self.setItemsCheckability(self.model, dimension=Data1D, checked=True)
968
969        elif index == 3:
970            # Unselect All 1-D
971            self.setItemsCheckability(self.model, dimension=Data1D, checked=False)
972
973        elif index == 4:
974            # Select All 2-D
975            self.setItemsCheckability(self.model, dimension=Data2D, checked=True)
976
977        elif index == 5:
978            # Unselect All 2-D
979            self.setItemsCheckability(self.model, dimension=Data2D, checked=False)
980
981        else:
982            msg = "Incorrect value in the Selection Option"
983            # Change this to a proper logging action
984            raise Exception(msg)
985
986    def contextMenu(self):
987        """
988        Define actions and layout of the right click context menu
989        """
990        # Create a custom menu based on actions defined in the UI file
991        self.context_menu = QtWidgets.QMenu(self)
992        self.context_menu.addAction(self.actionDataInfo)
993        self.context_menu.addAction(self.actionSaveAs)
994        self.context_menu.addAction(self.actionQuickPlot)
995        self.context_menu.addSeparator()
996        self.context_menu.addAction(self.actionQuick3DPlot)
997        self.context_menu.addAction(self.actionEditMask)
998        #self.context_menu.addSeparator()
999        #self.context_menu.addAction(self.actionFreezeResults)
1000        self.context_menu.addSeparator()
1001        self.context_menu.addAction(self.actionDelete)
1002
1003
1004        # Define the callbacks
1005        self.actionDataInfo.triggered.connect(self.showDataInfo)
1006        self.actionSaveAs.triggered.connect(self.saveDataAs)
1007        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
1008        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
1009        self.actionEditMask.triggered.connect(self.showEditDataMask)
1010        self.actionDelete.triggered.connect(self.deleteItem)
1011        self.actionFreezeResults.triggered.connect(self.freezeSelectedItems)
1012
1013    def onCustomContextMenu(self, position):
1014        """
1015        Show the right-click context menu in the data treeview
1016        """
1017        index = self.current_view.indexAt(position)
1018        proxy = self.current_view.model()
1019        model = proxy.sourceModel()
1020
1021        if not index.isValid():
1022            return
1023        model_item = model.itemFromIndex(proxy.mapToSource(index))
1024        # Find the mapped index
1025        orig_index = model_item.isCheckable()
1026        if not orig_index:
1027            return
1028        # Check the data to enable/disable actions
1029        is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
1030        self.actionQuick3DPlot.setEnabled(is_2D)
1031        self.actionEditMask.setEnabled(is_2D)
1032
1033        # Freezing
1034        # check that the selection has inner items
1035        freeze_enabled = False
1036        if model_item.parent() is not None:
1037            freeze_enabled = True
1038        self.actionFreezeResults.setEnabled(freeze_enabled)
1039
1040        # Fire up the menu
1041        self.context_menu.exec_(self.current_view.mapToGlobal(position))
1042
1043    def showDataInfo(self):
1044        """
1045        Show a simple read-only text edit with data information.
1046        """
1047        index = self.current_view.selectedIndexes()[0]
1048        proxy = self.current_view.model()
1049        model = proxy.sourceModel()
1050        model_item = model.itemFromIndex(proxy.mapToSource(index))
1051
1052        data = GuiUtils.dataFromItem(model_item)
1053        if isinstance(data, Data1D):
1054            text_to_show = GuiUtils.retrieveData1d(data)
1055            # Hardcoded sizes to enable full width rendering with default font
1056            self.txt_widget.resize(420,600)
1057        else:
1058            text_to_show = GuiUtils.retrieveData2d(data)
1059            # Hardcoded sizes to enable full width rendering with default font
1060            self.txt_widget.resize(700,600)
1061
1062        self.txt_widget.setReadOnly(True)
1063        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
1064        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
1065        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
1066        self.txt_widget.clear()
1067        self.txt_widget.insertPlainText(text_to_show)
1068
1069        self.txt_widget.show()
1070        # Move the slider all the way up, if present
1071        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
1072        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
1073
1074    def saveDataAs(self):
1075        """
1076        Save the data points as either txt or xml
1077        """
1078        index = self.current_view.selectedIndexes()[0]
1079        proxy = self.current_view.model()
1080        model = proxy.sourceModel()
1081        model_item = model.itemFromIndex(proxy.mapToSource(index))
1082
1083        data = GuiUtils.dataFromItem(model_item)
1084        if isinstance(data, Data1D):
1085            GuiUtils.saveData1D(data)
1086        else:
1087            GuiUtils.saveData2D(data)
1088
1089    def quickDataPlot(self):
1090        """
1091        Frozen plot - display an image of the plot
1092        """
1093        index = self.current_view.selectedIndexes()[0]
1094        proxy = self.current_view.model()
1095        model = proxy.sourceModel()
1096        model_item = model.itemFromIndex(proxy.mapToSource(index))
1097
1098        data = GuiUtils.dataFromItem(model_item)
1099
1100        method_name = 'Plotter'
1101        if isinstance(data, Data2D):
1102            method_name='Plotter2D'
1103
1104        self.new_plot = globals()[method_name](self, quickplot=True)
1105        self.new_plot.data = data
1106        #new_plot.plot(marker='o')
1107        self.new_plot.plot()
1108
1109        # Update the global plot counter
1110        title = "Plot " + data.name
1111        self.new_plot.setWindowTitle(title)
1112
1113        # Show the plot
1114        self.new_plot.show()
1115
1116    def quickData3DPlot(self):
1117        """
1118        Slowish 3D plot
1119        """
1120        index = self.current_view.selectedIndexes()[0]
1121        proxy = self.current_view.model()
1122        model = proxy.sourceModel()
1123        model_item = model.itemFromIndex(proxy.mapToSource(index))
1124
1125        data = GuiUtils.dataFromItem(model_item)
1126
1127        self.new_plot = Plotter2D(self, quickplot=True, dimension=3)
1128        self.new_plot.data = data
1129        self.new_plot.plot()
1130
1131        # Update the global plot counter
1132        title = "Plot " + data.name
1133        self.new_plot.setWindowTitle(title)
1134
1135        # Show the plot
1136        self.new_plot.show()
1137
1138    def extShowEditDataMask(self):
1139        self.showEditDataMask()
1140
1141    def showEditDataMask(self, data=None):
1142        """
1143        Mask Editor for 2D plots
1144        """
1145        try:
1146            if data is None or not isinstance(data, Data2D):
1147                index = self.current_view.selectedIndexes()[0]
1148                proxy = self.current_view.model()
1149                model = proxy.sourceModel()
1150                model_item = model.itemFromIndex(proxy.mapToSource(index))
1151
1152                data = GuiUtils.dataFromItem(model_item)
1153
1154            if data is None or not isinstance(data, Data2D):
1155                msg = QtWidgets.QMessageBox()
1156                msg.setIcon(QtWidgets.QMessageBox.Information)
1157                msg.setText("Error: cannot apply mask. \
1158                                Please select a 2D dataset.")
1159                msg.setStandardButtons(QtWidgets.QMessageBox.Cancel)
1160                msg.exec_()
1161                return
1162        except:
1163            msg = QtWidgets.QMessageBox()
1164            msg.setIcon(QtWidgets.QMessageBox.Information)
1165            msg.setText("Error: No dataset selected. \
1166                            Please select a 2D dataset.")
1167            msg.setStandardButtons(QtWidgets.QMessageBox.Cancel)
1168            msg.exec_()
1169            return
1170
1171        mask_editor = MaskEditor(self, data)
1172        # Modal dialog here.
1173        mask_editor.exec_()
1174
1175    def freezeItem(self, item=None):
1176        """
1177        Freeze given item
1178        """
1179        if item is None:
1180            return
1181        self.model.beginResetModel()
1182        new_item = self.cloneTheory(item)
1183        self.model.appendRow(new_item)
1184        self.model.endResetModel()
1185
1186    def freezeSelectedItems(self):
1187        """
1188        Freeze selected items
1189        """
1190        indices = self.treeView.selectedIndexes()
1191
1192        proxy = self.treeView.model()
1193        model = proxy.sourceModel()
1194
1195        for index in indices:
1196            row_index = proxy.mapToSource(index)
1197            item_to_copy = model.itemFromIndex(row_index)
1198            if item_to_copy and item_to_copy.isCheckable():
1199                self.freezeItem(item_to_copy)
1200
1201    def deleteItem(self):
1202        """
1203        Delete the current item
1204        """
1205        # Assure this is indeed wanted
1206        delete_msg = "This operation will delete the selected data sets " +\
1207                     "and all the dependents." +\
1208                     "\nDo you want to continue?"
1209        reply = QtWidgets.QMessageBox.question(self,
1210                                           'Warning',
1211                                           delete_msg,
1212                                           QtWidgets.QMessageBox.Yes,
1213                                           QtWidgets.QMessageBox.No)
1214
1215        if reply == QtWidgets.QMessageBox.No:
1216            return
1217
1218        # Every time a row is removed, the indices change, so we'll just remove
1219        # rows and keep calling selectedIndexes until it returns an empty list.
1220        indices = self.current_view.selectedIndexes()
1221
1222        proxy = self.current_view.model()
1223        model = proxy.sourceModel()
1224
1225        deleted_items = []
1226        deleted_names = []
1227
1228        while len(indices) > 0:
1229            index = indices[0]
1230            row_index = proxy.mapToSource(index)
1231            item_to_delete = model.itemFromIndex(row_index)
1232            if item_to_delete and item_to_delete.isCheckable():
1233                row = row_index.row()
1234
1235                # store the deleted item details so we can pass them on later
1236                deleted_names.append(item_to_delete.text())
1237                deleted_items.append(item_to_delete)
1238
1239                # Delete corresponding open plots
1240                self.closePlotsForItem(item_to_delete)
1241
1242                if item_to_delete.parent():
1243                    # We have a child item - delete from it
1244                    item_to_delete.parent().removeRow(row)
1245                else:
1246                    # delete directly from model
1247                    model.removeRow(row)
1248            indices = self.current_view.selectedIndexes()
1249
1250        # Let others know we deleted data
1251        self.communicator.dataDeletedSignal.emit(deleted_items)
1252
1253        # update stored_data
1254        self.manager.update_stored_data(deleted_names)
1255
1256    def closePlotsForItem(self, item):
1257        """
1258        Given standard item, close all its currently displayed plots
1259        """
1260        # item - HashableStandardItems of active plots
1261
1262        # {} -> 'Graph1' : HashableStandardItem()
1263        current_plot_items = {}
1264        for plot_name in PlotHelper.currentPlots():
1265            current_plot_items[plot_name] = PlotHelper.plotById(plot_name).item
1266
1267        # item and its hashable children
1268        items_being_deleted = []
1269        if item.rowCount() > 0:
1270            items_being_deleted = [item.child(n) for n in range(item.rowCount())
1271                                   if isinstance(item.child(n), GuiUtils.HashableStandardItem)]
1272        items_being_deleted.append(item)
1273        # Add the parent in case a child is selected
1274        if isinstance(item.parent(), GuiUtils.HashableStandardItem):
1275            items_being_deleted.append(item.parent())
1276
1277        # Compare plot items and items to delete
1278        plots_to_close = set(current_plot_items.values()) & set(items_being_deleted)
1279
1280        for plot_item in plots_to_close:
1281            for plot_name in current_plot_items.keys():
1282                if plot_item == current_plot_items[plot_name]:
1283                    plotter = PlotHelper.plotById(plot_name)
1284                    # try to delete the plot
1285                    try:
1286                        plotter.close()
1287                        #self.parent.workspace().removeSubWindow(plotter)
1288                        self.plot_widgets[plot_name].close()
1289                        self.plot_widgets.pop(plot_name, None)
1290                    except AttributeError as ex:
1291                        logging.error("Closing of %s failed:\n %s" % (plot_name, str(ex)))
1292
1293        pass # debugger anchor
1294
1295    def onAnalysisUpdate(self, new_perspective=""):
1296        """
1297        Update the perspective combo index based on passed string
1298        """
1299        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1300        self.cbFitting.blockSignals(True)
1301        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1302        self.cbFitting.blockSignals(False)
1303        pass
1304
1305    def loadComplete(self, output):
1306        """
1307        Post message to status bar and update the data manager
1308        """
1309        assert isinstance(output, tuple)
1310
1311        # Reset the model so the view gets updated.
1312        #self.model.reset()
1313        self.communicator.progressBarUpdateSignal.emit(-1)
1314
1315        output_data = output[0]
1316        message = output[1]
1317        # Notify the manager of the new data available
1318        self.communicator.statusBarUpdateSignal.emit(message)
1319        self.communicator.fileDataReceivedSignal.emit(output_data)
1320        self.manager.add_data(data_list=output_data)
1321
1322    def loadFailed(self, reason):
1323        print("File Load Failed with:\n", reason)
1324        pass
1325
1326    def updateModel(self, data, p_file):
1327        """
1328        Add data and Info fields to the model item
1329        """
1330        # Structure of the model
1331        # checkbox + basename
1332        #     |-------> Data.D object
1333        #     |-------> Info
1334        #                 |----> Title:
1335        #                 |----> Run:
1336        #                 |----> Type:
1337        #                 |----> Path:
1338        #                 |----> Process
1339        #                          |-----> process[0].name
1340        #     |-------> THEORIES
1341
1342        # Top-level item: checkbox with label
1343        checkbox_item = GuiUtils.HashableStandardItem()
1344        checkbox_item.setCheckable(True)
1345        checkbox_item.setCheckState(QtCore.Qt.Checked)
1346        checkbox_item.setText(os.path.basename(p_file))
1347
1348        # Add the actual Data1D/Data2D object
1349        object_item = GuiUtils.HashableStandardItem()
1350        object_item.setData(data)
1351
1352        checkbox_item.setChild(0, object_item)
1353
1354        # Add rows for display in the view
1355        info_item = GuiUtils.infoFromData(data)
1356
1357        # Set info_item as the first child
1358        checkbox_item.setChild(1, info_item)
1359
1360        # Caption for the theories
1361        checkbox_item.setChild(2, QtGui.QStandardItem("FIT RESULTS"))
1362
1363        # New row in the model
1364        self.model.beginResetModel()
1365        self.model.appendRow(checkbox_item)
1366        self.model.endResetModel()
1367
1368    def updateModelFromPerspective(self, model_item):
1369        """
1370        Receive an update model item from a perspective
1371        Make sure it is valid and if so, replace it in the model
1372        """
1373        # Assert the correct type
1374        if not isinstance(model_item, QtGui.QStandardItem):
1375            msg = "Wrong data type returned from calculations."
1376            raise AttributeError(msg)
1377
1378        # TODO: Assert other properties
1379
1380        # Reset the view
1381        ##self.model.reset()
1382        # Pass acting as a debugger anchor
1383        pass
1384
1385    def updateTheoryFromPerspective(self, model_item):
1386        """
1387        Receive an update theory item from a perspective
1388        Make sure it is valid and if so, replace/add in the model
1389        """
1390        # Assert the correct type
1391        if not isinstance(model_item, QtGui.QStandardItem):
1392            msg = "Wrong data type returned from calculations."
1393            raise AttributeError(msg)
1394
1395        # Check if there are any other items for this tab
1396        # If so, delete them
1397        current_tab_name = model_item.text()
1398        for current_index in range(self.theory_model.rowCount()):
1399            #if current_tab_name in self.theory_model.item(current_index).text():
1400            if current_tab_name == self.theory_model.item(current_index).text():
1401                self.theory_model.removeRow(current_index)
1402                break
1403
1404        # send in the new item
1405        self.theory_model.appendRow(model_item)
1406
1407    def deleteIntermediateTheoryPlotsByModelID(self, model_id):
1408        """Given a model's ID, deletes all items in the theory item model which reference the same ID. Useful in the
1409        case of intermediate results disappearing when changing calculations (in which case you don't want them to be
1410        retained in the list)."""
1411        items_to_delete = []
1412        for r in range(self.theory_model.rowCount()):
1413            item = self.theory_model.item(r, 0)
1414            data = item.child(0).data()
1415            if not hasattr(data, "id"):
1416                return
1417            match = GuiUtils.theory_plot_ID_pattern.match(data.id)
1418            if match:
1419                item_model_id = match.groups()[-1]
1420                if item_model_id == model_id:
1421                    # Only delete those identified as an intermediate plot
1422                    if match.groups()[2] not in (None, ""):
1423                        items_to_delete.append(item)
1424
1425        for item in items_to_delete:
1426            self.theory_model.removeRow(item.row())
Note: See TracBrowser for help on using the repository browser.