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

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

Finalize backward compatibility mode for project read for simple fitting

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