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

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

Batch page serialization/deserialization

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