source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 8137a02

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

Improved handling of 2d plot children. Refactored model tree search.

  • 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        if not dimension in types:
953            return
954
955        for index in range(model.rowCount()):
956            item = model.item(index)
957            if item.isCheckable() and item.checkState() != mode:
958                data = item.child(0).data()
959                if dimension is None or isinstance(data, dimension):
960                    item.setCheckState(mode)
961
962            items = list(GuiUtils.getChildrenFromItem(item))
963
964            for it in items:
965                if it.isCheckable() and it.checkState() != mode:
966                    data = it.child(0).data()
967                    if dimension is None or isinstance(data, dimension):
968                        it.setCheckState(mode)
969
970    def selectData(self, index):
971        """
972        Callback method for modifying the TreeView on Selection Options change
973        """
974        if not isinstance(index, int):
975            msg = "Incorrect type passed to DataExplorer.selectData()"
976            raise AttributeError(msg)
977
978        # Respond appropriately
979        if index == 0:
980            self.setItemsCheckability(self.model, checked=True)
981
982        elif index == 1:
983            # De-select All
984            self.setItemsCheckability(self.model, checked=False)
985
986        elif index == 2:
987            # Select All 1-D
988            self.setItemsCheckability(self.model, dimension=Data1D, checked=True)
989
990        elif index == 3:
991            # Unselect All 1-D
992            self.setItemsCheckability(self.model, dimension=Data1D, checked=False)
993
994        elif index == 4:
995            # Select All 2-D
996            self.setItemsCheckability(self.model, dimension=Data2D, checked=True)
997
998        elif index == 5:
999            # Unselect All 2-D
1000            self.setItemsCheckability(self.model, dimension=Data2D, checked=False)
1001
1002        else:
1003            msg = "Incorrect value in the Selection Option"
1004            # Change this to a proper logging action
1005            raise Exception(msg)
1006
1007    def contextMenu(self):
1008        """
1009        Define actions and layout of the right click context menu
1010        """
1011        # Create a custom menu based on actions defined in the UI file
1012        self.context_menu = QtWidgets.QMenu(self)
1013        self.context_menu.addAction(self.actionDataInfo)
1014        self.context_menu.addAction(self.actionSaveAs)
1015        self.context_menu.addAction(self.actionQuickPlot)
1016        self.context_menu.addSeparator()
1017        self.context_menu.addAction(self.actionQuick3DPlot)
1018        self.context_menu.addAction(self.actionEditMask)
1019        #self.context_menu.addSeparator()
1020        #self.context_menu.addAction(self.actionFreezeResults)
1021        self.context_menu.addSeparator()
1022        self.context_menu.addAction(self.actionDelete)
1023
1024
1025        # Define the callbacks
1026        self.actionDataInfo.triggered.connect(self.showDataInfo)
1027        self.actionSaveAs.triggered.connect(self.saveDataAs)
1028        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
1029        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
1030        self.actionEditMask.triggered.connect(self.showEditDataMask)
1031        self.actionDelete.triggered.connect(self.deleteItem)
1032        self.actionFreezeResults.triggered.connect(self.freezeSelectedItems)
1033
1034    def onCustomContextMenu(self, position):
1035        """
1036        Show the right-click context menu in the data treeview
1037        """
1038        index = self.current_view.indexAt(position)
1039        proxy = self.current_view.model()
1040        model = proxy.sourceModel()
1041
1042        if not index.isValid():
1043            return
1044        model_item = model.itemFromIndex(proxy.mapToSource(index))
1045        # Find the mapped index
1046        orig_index = model_item.isCheckable()
1047        if not orig_index:
1048            return
1049        # Check the data to enable/disable actions
1050        is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
1051        self.actionQuick3DPlot.setEnabled(is_2D)
1052        self.actionEditMask.setEnabled(is_2D)
1053
1054        # Freezing
1055        # check that the selection has inner items
1056        freeze_enabled = False
1057        if model_item.parent() is not None:
1058            freeze_enabled = True
1059        self.actionFreezeResults.setEnabled(freeze_enabled)
1060
1061        # Fire up the menu
1062        self.context_menu.exec_(self.current_view.mapToGlobal(position))
1063
1064    def showDataInfo(self):
1065        """
1066        Show a simple read-only text edit with data information.
1067        """
1068        index = self.current_view.selectedIndexes()[0]
1069        proxy = self.current_view.model()
1070        model = proxy.sourceModel()
1071        model_item = model.itemFromIndex(proxy.mapToSource(index))
1072
1073        data = GuiUtils.dataFromItem(model_item)
1074        if isinstance(data, Data1D):
1075            text_to_show = GuiUtils.retrieveData1d(data)
1076            # Hardcoded sizes to enable full width rendering with default font
1077            self.txt_widget.resize(420,600)
1078        else:
1079            text_to_show = GuiUtils.retrieveData2d(data)
1080            # Hardcoded sizes to enable full width rendering with default font
1081            self.txt_widget.resize(700,600)
1082
1083        self.txt_widget.setReadOnly(True)
1084        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
1085        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
1086        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
1087        self.txt_widget.clear()
1088        self.txt_widget.insertPlainText(text_to_show)
1089
1090        self.txt_widget.show()
1091        # Move the slider all the way up, if present
1092        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
1093        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
1094
1095    def saveDataAs(self):
1096        """
1097        Save the data points as either txt or xml
1098        """
1099        index = self.current_view.selectedIndexes()[0]
1100        proxy = self.current_view.model()
1101        model = proxy.sourceModel()
1102        model_item = model.itemFromIndex(proxy.mapToSource(index))
1103
1104        data = GuiUtils.dataFromItem(model_item)
1105        if isinstance(data, Data1D):
1106            GuiUtils.saveData1D(data)
1107        else:
1108            GuiUtils.saveData2D(data)
1109
1110    def quickDataPlot(self):
1111        """
1112        Frozen plot - display an image of the plot
1113        """
1114        index = self.current_view.selectedIndexes()[0]
1115        proxy = self.current_view.model()
1116        model = proxy.sourceModel()
1117        model_item = model.itemFromIndex(proxy.mapToSource(index))
1118
1119        data = GuiUtils.dataFromItem(model_item)
1120
1121        method_name = 'Plotter'
1122        if isinstance(data, Data2D):
1123            method_name='Plotter2D'
1124
1125        self.new_plot = globals()[method_name](self, quickplot=True)
1126        self.new_plot.data = data
1127        #new_plot.plot(marker='o')
1128        self.new_plot.plot()
1129
1130        # Update the global plot counter
1131        title = "Plot " + data.name
1132        self.new_plot.setWindowTitle(title)
1133
1134        # Show the plot
1135        self.new_plot.show()
1136
1137    def quickData3DPlot(self):
1138        """
1139        Slowish 3D plot
1140        """
1141        index = self.current_view.selectedIndexes()[0]
1142        proxy = self.current_view.model()
1143        model = proxy.sourceModel()
1144        model_item = model.itemFromIndex(proxy.mapToSource(index))
1145
1146        data = GuiUtils.dataFromItem(model_item)
1147
1148        self.new_plot = Plotter2D(self, quickplot=True, dimension=3)
1149        self.new_plot.data = data
1150        self.new_plot.plot()
1151
1152        # Update the global plot counter
1153        title = "Plot " + data.name
1154        self.new_plot.setWindowTitle(title)
1155
1156        # Show the plot
1157        self.new_plot.show()
1158
1159    def extShowEditDataMask(self):
1160        self.showEditDataMask()
1161
1162    def showEditDataMask(self, data=None):
1163        """
1164        Mask Editor for 2D plots
1165        """
1166        try:
1167            if data is None or not isinstance(data, Data2D):
1168                index = self.current_view.selectedIndexes()[0]
1169                proxy = self.current_view.model()
1170                model = proxy.sourceModel()
1171                model_item = model.itemFromIndex(proxy.mapToSource(index))
1172
1173                data = GuiUtils.dataFromItem(model_item)
1174
1175            if data is None or not isinstance(data, Data2D):
1176                msg = QtWidgets.QMessageBox()
1177                msg.setIcon(QtWidgets.QMessageBox.Information)
1178                msg.setText("Error: cannot apply mask. \
1179                                Please select a 2D dataset.")
1180                msg.setStandardButtons(QtWidgets.QMessageBox.Cancel)
1181                msg.exec_()
1182                return
1183        except:
1184            msg = QtWidgets.QMessageBox()
1185            msg.setIcon(QtWidgets.QMessageBox.Information)
1186            msg.setText("Error: No dataset selected. \
1187                            Please select a 2D dataset.")
1188            msg.setStandardButtons(QtWidgets.QMessageBox.Cancel)
1189            msg.exec_()
1190            return
1191
1192        mask_editor = MaskEditor(self, data)
1193        # Modal dialog here.
1194        mask_editor.exec_()
1195
1196    def freezeItem(self, item=None):
1197        """
1198        Freeze given item
1199        """
1200        if item is None:
1201            return
1202        self.model.beginResetModel()
1203        new_item = self.cloneTheory(item)
1204        self.model.appendRow(new_item)
1205        self.model.endResetModel()
1206
1207    def freezeSelectedItems(self):
1208        """
1209        Freeze selected items
1210        """
1211        indices = self.treeView.selectedIndexes()
1212
1213        proxy = self.treeView.model()
1214        model = proxy.sourceModel()
1215
1216        for index in indices:
1217            row_index = proxy.mapToSource(index)
1218            item_to_copy = model.itemFromIndex(row_index)
1219            if item_to_copy and item_to_copy.isCheckable():
1220                self.freezeItem(item_to_copy)
1221
1222    def deleteItem(self):
1223        """
1224        Delete the current item
1225        """
1226        # Assure this is indeed wanted
1227        delete_msg = "This operation will delete the selected data sets " +\
1228                     "and all the dependents." +\
1229                     "\nDo you want to continue?"
1230        reply = QtWidgets.QMessageBox.question(self,
1231                                           'Warning',
1232                                           delete_msg,
1233                                           QtWidgets.QMessageBox.Yes,
1234                                           QtWidgets.QMessageBox.No)
1235
1236        if reply == QtWidgets.QMessageBox.No:
1237            return
1238
1239        # Every time a row is removed, the indices change, so we'll just remove
1240        # rows and keep calling selectedIndexes until it returns an empty list.
1241        indices = self.current_view.selectedIndexes()
1242
1243        proxy = self.current_view.model()
1244        model = proxy.sourceModel()
1245
1246        deleted_items = []
1247        deleted_names = []
1248
1249        while len(indices) > 0:
1250            index = indices[0]
1251            row_index = proxy.mapToSource(index)
1252            item_to_delete = model.itemFromIndex(row_index)
1253            if item_to_delete and item_to_delete.isCheckable():
1254                row = row_index.row()
1255
1256                # store the deleted item details so we can pass them on later
1257                deleted_names.append(item_to_delete.text())
1258                deleted_items.append(item_to_delete)
1259
1260                # Delete corresponding open plots
1261                self.closePlotsForItem(item_to_delete)
1262
1263                if item_to_delete.parent():
1264                    # We have a child item - delete from it
1265                    item_to_delete.parent().removeRow(row)
1266                else:
1267                    # delete directly from model
1268                    model.removeRow(row)
1269            indices = self.current_view.selectedIndexes()
1270
1271        # Let others know we deleted data
1272        self.communicator.dataDeletedSignal.emit(deleted_items)
1273
1274        # update stored_data
1275        self.manager.update_stored_data(deleted_names)
1276
1277    def closePlotsForItem(self, item):
1278        """
1279        Given standard item, close all its currently displayed plots
1280        """
1281        # item - HashableStandardItems of active plots
1282
1283        # {} -> 'Graph1' : HashableStandardItem()
1284        current_plot_items = {}
1285        for plot_name in PlotHelper.currentPlots():
1286            current_plot_items[plot_name] = PlotHelper.plotById(plot_name).item
1287
1288        # item and its hashable children
1289        items_being_deleted = []
1290        if item.rowCount() > 0:
1291            items_being_deleted = [item.child(n) for n in range(item.rowCount())
1292                                   if isinstance(item.child(n), GuiUtils.HashableStandardItem)]
1293        items_being_deleted.append(item)
1294        # Add the parent in case a child is selected
1295        if isinstance(item.parent(), GuiUtils.HashableStandardItem):
1296            items_being_deleted.append(item.parent())
1297
1298        # Compare plot items and items to delete
1299        plots_to_close = set(current_plot_items.values()) & set(items_being_deleted)
1300
1301        for plot_item in plots_to_close:
1302            for plot_name in current_plot_items.keys():
1303                if plot_item == current_plot_items[plot_name]:
1304                    plotter = PlotHelper.plotById(plot_name)
1305                    # try to delete the plot
1306                    try:
1307                        plotter.close()
1308                        #self.parent.workspace().removeSubWindow(plotter)
1309                        self.plot_widgets[plot_name].close()
1310                        self.plot_widgets.pop(plot_name, None)
1311                    except AttributeError as ex:
1312                        logging.error("Closing of %s failed:\n %s" % (plot_name, str(ex)))
1313
1314        pass # debugger anchor
1315
1316    def onAnalysisUpdate(self, new_perspective=""):
1317        """
1318        Update the perspective combo index based on passed string
1319        """
1320        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1321        self.cbFitting.blockSignals(True)
1322        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1323        self.cbFitting.blockSignals(False)
1324        pass
1325
1326    def loadComplete(self, output):
1327        """
1328        Post message to status bar and update the data manager
1329        """
1330        assert isinstance(output, tuple)
1331
1332        # Reset the model so the view gets updated.
1333        #self.model.reset()
1334        self.communicator.progressBarUpdateSignal.emit(-1)
1335
1336        output_data = output[0]
1337        message = output[1]
1338        # Notify the manager of the new data available
1339        self.communicator.statusBarUpdateSignal.emit(message)
1340        self.communicator.fileDataReceivedSignal.emit(output_data)
1341        self.manager.add_data(data_list=output_data)
1342
1343    def loadFailed(self, reason):
1344        print("File Load Failed with:\n", reason)
1345        pass
1346
1347    def updateModel(self, data, p_file):
1348        """
1349        Add data and Info fields to the model item
1350        """
1351        # Structure of the model
1352        # checkbox + basename
1353        #     |-------> Data.D object
1354        #     |-------> Info
1355        #                 |----> Title:
1356        #                 |----> Run:
1357        #                 |----> Type:
1358        #                 |----> Path:
1359        #                 |----> Process
1360        #                          |-----> process[0].name
1361        #     |-------> THEORIES
1362
1363        # Top-level item: checkbox with label
1364        checkbox_item = GuiUtils.HashableStandardItem()
1365        checkbox_item.setCheckable(True)
1366        checkbox_item.setCheckState(QtCore.Qt.Checked)
1367        checkbox_item.setText(os.path.basename(p_file))
1368
1369        # Add the actual Data1D/Data2D object
1370        object_item = GuiUtils.HashableStandardItem()
1371        object_item.setData(data)
1372
1373        checkbox_item.setChild(0, object_item)
1374
1375        # Add rows for display in the view
1376        info_item = GuiUtils.infoFromData(data)
1377
1378        # Set info_item as the first child
1379        checkbox_item.setChild(1, info_item)
1380
1381        # Caption for the theories
1382        checkbox_item.setChild(2, QtGui.QStandardItem("FIT RESULTS"))
1383
1384        # New row in the model
1385        self.model.beginResetModel()
1386        self.model.appendRow(checkbox_item)
1387        self.model.endResetModel()
1388
1389    def updateModelFromPerspective(self, model_item):
1390        """
1391        Receive an update model item from a perspective
1392        Make sure it is valid and if so, replace it in the model
1393        """
1394        # Assert the correct type
1395        if not isinstance(model_item, QtGui.QStandardItem):
1396            msg = "Wrong data type returned from calculations."
1397            raise AttributeError(msg)
1398
1399        # TODO: Assert other properties
1400
1401        # Reset the view
1402        ##self.model.reset()
1403        # Pass acting as a debugger anchor
1404        pass
1405
1406    def updateTheoryFromPerspective(self, model_item):
1407        """
1408        Receive an update theory item from a perspective
1409        Make sure it is valid and if so, replace/add in the model
1410        """
1411        # Assert the correct type
1412        if not isinstance(model_item, QtGui.QStandardItem):
1413            msg = "Wrong data type returned from calculations."
1414            raise AttributeError(msg)
1415
1416        # Check if there are any other items for this tab
1417        # If so, delete them
1418        current_tab_name = model_item.text()
1419        for current_index in range(self.theory_model.rowCount()):
1420            if current_tab_name == self.theory_model.item(current_index).text():
1421                self.theory_model.removeRow(current_index)
1422                break
1423        # send in the new item
1424        self.theory_model.appendRow(model_item)
1425
1426    def deleteIntermediateTheoryPlotsByModelID(self, model_id):
1427        """Given a model's ID, deletes all items in the theory item model which reference the same ID. Useful in the
1428        case of intermediate results disappearing when changing calculations (in which case you don't want them to be
1429        retained in the list)."""
1430        items_to_delete = []
1431        for r in range(self.theory_model.rowCount()):
1432            item = self.theory_model.item(r, 0)
1433            data = item.child(0).data()
1434            if not hasattr(data, "id"):
1435                return
1436            match = GuiUtils.theory_plot_ID_pattern.match(data.id)
1437            if match:
1438                item_model_id = match.groups()[-1]
1439                if item_model_id == model_id:
1440                    # Only delete those identified as an intermediate plot
1441                    if match.groups()[2] not in (None, ""):
1442                        items_to_delete.append(item)
1443
1444        for item in items_to_delete:
1445            self.theory_model.removeRow(item.row())
Note: See TracBrowser for help on using the repository browser.