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

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

Working project load/save. new format only. SASVIEW-983/984

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