source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 6bc0840

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

Added "Close All Plots" to main window/Window menu

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