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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 30bed93 was 30bed93, checked in by GitHub <noreply@…>, 13 months ago

Merge pull request #181 from SasView?/ESS_GUI_poly_plot2

plot polydispersity (SASVIEW-1035 and trac ticket 17)

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