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

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

Working project load/save. new format only. SASVIEW-983/984

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