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

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

Automatically show sector/annulus/box plots SASVIEW-980

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