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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 376a939 was 376a939, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Fixed deserialization of batch pages.

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