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

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

Be careful with standard item lifetime. SASVIEW-1231

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