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

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

Analysis only for Fitting, project save on other perspectives saves
datasets. Fixed tab deletion on data removal for batch tabs.

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