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

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

Minor fixes for SVS loader/converter logic. SASVIEW-1237

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