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

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

Pay attention to 2D charts lifetime. SASVIEW-1175

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