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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 96e8e39 was 96e8e39, checked in by rozyczko <piotrrozyczko@…>, 6 years ago

Don't assume all perspectives have 'title' attribute. Some may only have
the standard 'windowTitle'

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