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

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

Restore 2D data chart as well. SASVIEW-1221

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