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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9d7cb19 was 9d7cb19, checked in by piotr, 5 years ago

File/data selection should respond to activation, not index changed.
SASVIEW-1188

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