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

ESS_GUIESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_sync_sascalc
Last change on this file since a8e6394 was d9e7792, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Show Plot now restores minimized plots. SASVIEW-1221 == trac#13
Added "Minimize all plots" to Window menu
Changed draw → draw_idle to avoid weird numpy linalg errors

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