source: sasview/src/sas/qtgui/MainWindow/DataExplorer.py @ 5584dee

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

Merged ESS_GUI_image_viewer

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