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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 859d960 was 859d960, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 9 months ago

Directory/file load location is now separate from save report and from
save/load project SASVIEW-1186

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