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

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

Working version of Save/Load? Analysis. SASVIEW-983.
Changed the default behaviour of Category/Model? combos:
Selecting a category does not pre-select the first model now.

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