source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 722b7d6

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

Minor changes after PK's CR + minor refactoring.

  • Property mode set to 100644
File size: 53.9 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=None):
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        msg = QtWidgets.QMessageBox()
1174        msg.setIcon(QtWidgets.QMessageBox.Information)
1175        msg.setText("Error: cannot apply mask.\n"+
1176                    "Please select a 2D dataset.")
1177        msg.setStandardButtons(QtWidgets.QMessageBox.Ok)
1178
1179        try:
1180            if data is None or not isinstance(data, Data2D):
1181                # if data wasn't passed - try to get it from
1182                # the currently selected item
1183                index = self.current_view.selectedIndexes()[0]
1184                proxy = self.current_view.model()
1185                model = proxy.sourceModel()
1186                model_item = model.itemFromIndex(proxy.mapToSource(index))
1187
1188                data = GuiUtils.dataFromItem(model_item)
1189
1190            if data is None or not isinstance(data, Data2D):
1191                # If data is still not right, complain
1192                msg.exec_()
1193                return
1194        except:
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        # Append a "unique" descriptor to the name
1221        time_bit = str(time.time())[7:-1].replace('.', '')
1222        new_name = data.name + '_@' + time_bit
1223        # Change the underlying data so it is no longer a theory
1224        try:
1225            data.is_data = True
1226            data.symbol = 'Circle'
1227        except AttributeError:
1228            #no data here, pass
1229            pass
1230        new_item = GuiUtils.createModelItemWithPlot(data, new_name)
1231
1232        self.model.appendRow(new_item)
1233        self.model.endResetModel()
1234
1235    def freezeSelectedItems(self):
1236        """
1237        Freeze selected items
1238        """
1239        indices = self.treeView.selectedIndexes()
1240
1241        proxy = self.treeView.model()
1242        model = proxy.sourceModel()
1243
1244        for index in indices:
1245            row_index = proxy.mapToSource(index)
1246            item_to_copy = model.itemFromIndex(row_index)
1247            if item_to_copy and item_to_copy.isCheckable():
1248                self.freezeItem(item_to_copy)
1249
1250    def deleteItem(self):
1251        """
1252        Delete the current item
1253        """
1254        # Assure this is indeed wanted
1255        delete_msg = "This operation will delete the selected data sets " +\
1256                     "and all the dependents." +\
1257                     "\nDo you want to continue?"
1258        reply = QtWidgets.QMessageBox.question(self,
1259                                           'Warning',
1260                                           delete_msg,
1261                                           QtWidgets.QMessageBox.Yes,
1262                                           QtWidgets.QMessageBox.No)
1263
1264        if reply == QtWidgets.QMessageBox.No:
1265            return
1266
1267        # Every time a row is removed, the indices change, so we'll just remove
1268        # rows and keep calling selectedIndexes until it returns an empty list.
1269        indices = self.current_view.selectedIndexes()
1270
1271        proxy = self.current_view.model()
1272        model = proxy.sourceModel()
1273
1274        deleted_items = []
1275        deleted_names = []
1276
1277        while len(indices) > 0:
1278            index = indices[0]
1279            row_index = proxy.mapToSource(index)
1280            item_to_delete = model.itemFromIndex(row_index)
1281            if item_to_delete and item_to_delete.isCheckable():
1282                row = row_index.row()
1283
1284                # store the deleted item details so we can pass them on later
1285                deleted_names.append(item_to_delete.text())
1286                deleted_items.append(item_to_delete)
1287
1288                # Delete corresponding open plots
1289                self.closePlotsForItem(item_to_delete)
1290
1291                if item_to_delete.parent():
1292                    # We have a child item - delete from it
1293                    item_to_delete.parent().removeRow(row)
1294                else:
1295                    # delete directly from model
1296                    model.removeRow(row)
1297            indices = self.current_view.selectedIndexes()
1298
1299        # Let others know we deleted data
1300        self.communicator.dataDeletedSignal.emit(deleted_items)
1301
1302        # update stored_data
1303        self.manager.update_stored_data(deleted_names)
1304
1305    def closeAllPlots(self):
1306        """
1307        Close all currently displayed plots
1308        """
1309
1310        for plot_id in PlotHelper.currentPlots():
1311            try:
1312                plotter = PlotHelper.plotById(plot_id)
1313                plotter.close()
1314                self.plot_widgets[plot_id].close()
1315                self.plot_widgets.pop(plot_id, None)
1316            except AttributeError as ex:
1317                logging.error("Closing of %s failed:\n %s" % (plot_id, str(ex)))
1318
1319    def closePlotsForItem(self, item):
1320        """
1321        Given standard item, close all its currently displayed plots
1322        """
1323        # item - HashableStandardItems of active plots
1324
1325        # {} -> 'Graph1' : HashableStandardItem()
1326        current_plot_items = {}
1327        for plot_name in PlotHelper.currentPlots():
1328            current_plot_items[plot_name] = PlotHelper.plotById(plot_name).item
1329
1330        # item and its hashable children
1331        items_being_deleted = []
1332        if item.rowCount() > 0:
1333            items_being_deleted = [item.child(n) for n in range(item.rowCount())
1334                                   if isinstance(item.child(n), GuiUtils.HashableStandardItem)]
1335        items_being_deleted.append(item)
1336        # Add the parent in case a child is selected
1337        if isinstance(item.parent(), GuiUtils.HashableStandardItem):
1338            items_being_deleted.append(item.parent())
1339
1340        # Compare plot items and items to delete
1341        plots_to_close = set(current_plot_items.values()) & set(items_being_deleted)
1342
1343        for plot_item in plots_to_close:
1344            for plot_name in current_plot_items.keys():
1345                if plot_item == current_plot_items[plot_name]:
1346                    plotter = PlotHelper.plotById(plot_name)
1347                    # try to delete the plot
1348                    try:
1349                        plotter.close()
1350                        #self.parent.workspace().removeSubWindow(plotter)
1351                        self.plot_widgets[plot_name].close()
1352                        self.plot_widgets.pop(plot_name, None)
1353                    except AttributeError as ex:
1354                        logging.error("Closing of %s failed:\n %s" % (plot_name, str(ex)))
1355
1356        pass # debugger anchor
1357
1358    def onAnalysisUpdate(self, new_perspective=""):
1359        """
1360        Update the perspective combo index based on passed string
1361        """
1362        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1363        self.cbFitting.blockSignals(True)
1364        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1365        self.cbFitting.blockSignals(False)
1366        pass
1367
1368    def loadComplete(self, output):
1369        """
1370        Post message to status bar and update the data manager
1371        """
1372        assert isinstance(output, tuple)
1373
1374        # Reset the model so the view gets updated.
1375        #self.model.reset()
1376        self.communicator.progressBarUpdateSignal.emit(-1)
1377
1378        output_data = output[0]
1379        message = output[1]
1380        # Notify the manager of the new data available
1381        self.communicator.statusBarUpdateSignal.emit(message)
1382        self.communicator.fileDataReceivedSignal.emit(output_data)
1383        self.manager.add_data(data_list=output_data)
1384
1385    def loadFailed(self, reason):
1386        print("File Load Failed with:\n", reason)
1387        pass
1388
1389    def updateModel(self, data, p_file):
1390        """
1391        Add data and Info fields to the model item
1392        """
1393        # Structure of the model
1394        # checkbox + basename
1395        #     |-------> Data.D object
1396        #     |-------> Info
1397        #                 |----> Title:
1398        #                 |----> Run:
1399        #                 |----> Type:
1400        #                 |----> Path:
1401        #                 |----> Process
1402        #                          |-----> process[0].name
1403        #     |-------> THEORIES
1404
1405        # Top-level item: checkbox with label
1406        checkbox_item = GuiUtils.HashableStandardItem()
1407        checkbox_item.setCheckable(True)
1408        checkbox_item.setCheckState(QtCore.Qt.Checked)
1409        checkbox_item.setText(os.path.basename(p_file))
1410
1411        # Add the actual Data1D/Data2D object
1412        object_item = GuiUtils.HashableStandardItem()
1413        object_item.setData(data)
1414
1415        checkbox_item.setChild(0, object_item)
1416
1417        # Add rows for display in the view
1418        info_item = GuiUtils.infoFromData(data)
1419
1420        # Set info_item as the first child
1421        checkbox_item.setChild(1, info_item)
1422
1423        # Caption for the theories
1424        checkbox_item.setChild(2, QtGui.QStandardItem("FIT RESULTS"))
1425
1426        # New row in the model
1427        self.model.beginResetModel()
1428        self.model.appendRow(checkbox_item)
1429        self.model.endResetModel()
1430
1431    def updateModelFromPerspective(self, model_item):
1432        """
1433        Receive an update model item from a perspective
1434        Make sure it is valid and if so, replace it in the model
1435        """
1436        # Assert the correct type
1437        if not isinstance(model_item, QtGui.QStandardItem):
1438            msg = "Wrong data type returned from calculations."
1439            raise AttributeError(msg)
1440
1441        # TODO: Assert other properties
1442
1443        # Reset the view
1444        ##self.model.reset()
1445        # Pass acting as a debugger anchor
1446        pass
1447
1448    def updateTheoryFromPerspective(self, model_item):
1449        """
1450        Receive an update theory item from a perspective
1451        Make sure it is valid and if so, replace/add in the model
1452        """
1453        # Assert the correct type
1454        if not isinstance(model_item, QtGui.QStandardItem):
1455            msg = "Wrong data type returned from calculations."
1456            raise AttributeError(msg)
1457
1458        # Check if there are any other items for this tab
1459        # If so, delete them
1460        current_tab_name = model_item.text()
1461        for current_index in range(self.theory_model.rowCount()):
1462            if current_tab_name == self.theory_model.item(current_index).text():
1463                self.theory_model.removeRow(current_index)
1464                break
1465        # send in the new item
1466        self.theory_model.appendRow(model_item)
1467
1468    def deleteIntermediateTheoryPlotsByModelID(self, model_id):
1469        """Given a model's ID, deletes all items in the theory item model which reference the same ID. Useful in the
1470        case of intermediate results disappearing when changing calculations (in which case you don't want them to be
1471        retained in the list)."""
1472        items_to_delete = []
1473        for r in range(self.theory_model.rowCount()):
1474            item = self.theory_model.item(r, 0)
1475            data = item.child(0).data()
1476            if not hasattr(data, "id"):
1477                return
1478            match = GuiUtils.theory_plot_ID_pattern.match(data.id)
1479            if match:
1480                item_model_id = match.groups()[-1]
1481                if item_model_id == model_id:
1482                    # Only delete those identified as an intermediate plot
1483                    if match.groups()[2] not in (None, ""):
1484                        items_to_delete.append(item)
1485
1486        for item in items_to_delete:
1487            self.theory_model.removeRow(item.row())
Note: See TracBrowser for help on using the repository browser.