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

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

Save status of data explorer

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