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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9ce69ec was 9ce69ec, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Replaced 'smart' plot generation with explicit plot requests on "Show Plot". SASVIEW-1018

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