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

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

Added C&S tab serialization.

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