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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 685e0e3 was 685e0e3, checked in by piotr, 6 years ago

Fix for frozen theories not showing correct plots. SASVIEW-978
Fix for Linear Fit not working properly after recent changes.

  • Property mode set to 100644
File size: 45.2 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
45        # Secondary model for keeping frozen data sets
46        self.theory_model = QtGui.QStandardItemModel(self)
[f721030]47
48        # GuiManager is the actual parent, but we needed to also pass the QMainWindow
49        # in order to set the widget parentage properly.
50        self.parent = guimanager
51        self.loader = Loader()
[630155bd]52        self.manager = manager if manager is not None else DataManager()
[4992ff2]53        self.txt_widget = QtWidgets.QTextEdit(None)
[f721030]54
[481ff26]55        # Be careful with twisted threads.
[4992ff2]56        self.mutex = QtCore.QMutex()
[481ff26]57
[d9150d8]58        # Plot widgets {name:widget}, required to keep track of plots shown as MDI subwindows
59        self.plot_widgets = {}
60
61        # Active plots {id:Plotter1D/2D}, required to keep track of currently displayed plots
[7d077d1]62        self.active_plots = {}
[8cb6cd6]63
[f721030]64        # Connect the buttons
65        self.cmdLoad.clicked.connect(self.loadFile)
[f82ab8c]66        self.cmdDeleteData.clicked.connect(self.deleteFile)
67        self.cmdDeleteTheory.clicked.connect(self.deleteTheory)
68        self.cmdFreeze.clicked.connect(self.freezeTheory)
[f721030]69        self.cmdSendTo.clicked.connect(self.sendData)
[1042dba]70        self.cmdNew.clicked.connect(self.newPlot)
[0268aed]71        self.cmdNew_2.clicked.connect(self.newPlot)
[8cb6cd6]72        self.cmdAppend.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
89        self.cbSelect.currentIndexChanged.connect(self.selectData)
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)
[d5c5d3d]99
[8cb6cd6]100        self.cbgraph.editTextChanged.connect(self.enableGraphCombo)
101        self.cbgraph.currentIndexChanged.connect(self.enableGraphCombo)
[f721030]102
103        # Proxy model for showing a subset of Data1D/Data2D content
[4992ff2]104        self.data_proxy = QtCore.QSortFilterProxyModel(self)
[481ff26]105        self.data_proxy.setSourceModel(self.model)
106
107        # Don't show "empty" rows with data objects
108        self.data_proxy.setFilterRegExp(r"[^()]")
[f721030]109
110        # The Data viewer is QTreeView showing the proxy model
[481ff26]111        self.treeView.setModel(self.data_proxy)
112
113        # Proxy model for showing a subset of Theory content
[4992ff2]114        self.theory_proxy = QtCore.QSortFilterProxyModel(self)
[481ff26]115        self.theory_proxy.setSourceModel(self.theory_model)
116
117        # Don't show "empty" rows with data objects
118        self.theory_proxy.setFilterRegExp(r"[^()]")
[f721030]119
[f82ab8c]120        # Theory model view
[481ff26]121        self.freezeView.setModel(self.theory_proxy)
[f82ab8c]122
[8cb6cd6]123        self.enableGraphCombo(None)
124
[cbcdd2c]125        # Current view on model
126        self.current_view = self.treeView
127
[f82ab8c]128    def closeEvent(self, event):
129        """
130        Overwrite the close event - no close!
131        """
132        event.ignore()
[1042dba]133
[cbcdd2c]134    def onTabSwitch(self, index):
135        """ Callback for tab switching signal """
136        if index == 0:
137            self.current_view = self.treeView
138        else:
139            self.current_view = self.freezeView
140
[481ff26]141    def displayHelp(self):
142        """
143        Show the "Loading data" section of help
144        """
[aed0532]145        tree_location = "/user/qtgui/MainWindow/data_explorer_help.html"
[e90988c]146        self.parent.showHelp(tree_location)
[481ff26]147
[8cb6cd6]148    def enableGraphCombo(self, combo_text):
149        """
150        Enables/disables "Assign Plot" elements
151        """
152        self.cbgraph.setEnabled(len(PlotHelper.currentPlots()) > 0)
153        self.cmdAppend.setEnabled(len(PlotHelper.currentPlots()) > 0)
154
[83d6249]155    def initPerspectives(self):
156        """
157        Populate the Perspective combobox and define callbacks
158        """
[b3e8629]159        available_perspectives = sorted([p for p in list(Perspectives.PERSPECTIVES.keys())])
[1970780]160        if available_perspectives:
161            self.cbFitting.clear()
162            self.cbFitting.addItems(available_perspectives)
[83d6249]163        self.cbFitting.currentIndexChanged.connect(self.updatePerspectiveCombo)
164        # Set the index so we see the default (Fitting)
[d4881f6a]165        self.cbFitting.setCurrentIndex(self.cbFitting.findText(DEFAULT_PERSPECTIVE))
[83d6249]166
[1970780]167    def _perspective(self):
168        """
169        Returns the current perspective
170        """
171        return self.parent.perspective()
172
[f82ab8c]173    def loadFromURL(self, url):
174        """
175        Threaded file load
176        """
177        load_thread = threads.deferToThread(self.readData, url)
178        load_thread.addCallback(self.loadComplete)
[7fb471d]179        load_thread.addErrback(self.loadFailed)
[5032ea68]180
181    def loadFile(self, event=None):
[f721030]182        """
183        Called when the "Load" button pressed.
[481ff26]184        Opens the Qt "Open File..." dialog
[f721030]185        """
186        path_str = self.chooseFiles()
187        if not path_str:
188            return
[f82ab8c]189        self.loadFromURL(path_str)
[f721030]190
[5032ea68]191    def loadFolder(self, event=None):
[f721030]192        """
[5032ea68]193        Called when the "File/Load Folder" menu item chosen.
[481ff26]194        Opens the Qt "Open Folder..." dialog
[f721030]195        """
[7969b9c]196        folder = QtWidgets.QFileDialog.getExistingDirectory(self, "Choose a directory", "",
[4992ff2]197              QtWidgets.QFileDialog.ShowDirsOnly | QtWidgets.QFileDialog.DontUseNativeDialog)
[481ff26]198        if folder is None:
[5032ea68]199            return
200
[481ff26]201        folder = str(folder)
[f721030]202
[481ff26]203        if not os.path.isdir(folder):
[5032ea68]204            return
[f721030]205
[5032ea68]206        # get content of dir into a list
[481ff26]207        path_str = [os.path.join(os.path.abspath(folder), filename)
[e540cd2]208                    for filename in os.listdir(folder)]
[f721030]209
[f82ab8c]210        self.loadFromURL(path_str)
[5032ea68]211
[630155bd]212    def loadProject(self):
213        """
214        Called when the "Open Project" menu item chosen.
215        """
216        kwargs = {
217            'parent'    : self,
218            'caption'   : 'Open Project',
219            'filter'    : 'Project (*.json);;All files (*.*)',
[4992ff2]220            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
[630155bd]221        }
[fbfc488]222        filename = QtWidgets.QFileDialog.getOpenFileName(**kwargs)[0]
[630155bd]223        if filename:
224            load_thread = threads.deferToThread(self.readProject, filename)
225            load_thread.addCallback(self.readProjectComplete)
[7fb471d]226            load_thread.addErrback(self.readProjectFailed)
227
[4992ff2]228    def loadFailed(self, reason):
229        """
230        """
231        print("file load FAILED: ", reason)
232        pass
233
[7fb471d]234    def readProjectFailed(self, reason):
235        """
236        """
237        print("readProjectFailed FAILED: ", reason)
238        pass
[630155bd]239
240    def readProject(self, filename):
241        self.communicator.statusBarUpdateSignal.emit("Loading Project... %s" % os.path.basename(filename))
242        try:
243            manager = DataManager()
244            with open(filename, 'r') as infile:
245                manager.load_from_readable(infile)
246
247            self.communicator.statusBarUpdateSignal.emit("Loaded Project: %s" % os.path.basename(filename))
248            return manager
249
250        except:
251            self.communicator.statusBarUpdateSignal.emit("Failed: %s" % os.path.basename(filename))
252            raise
253
254    def readProjectComplete(self, manager):
255        self.model.clear()
256
257        self.manager.assign(manager)
[4992ff2]258        self.model.beginResetModel()
[b3e8629]259        for id, item in self.manager.get_all_data().items():
[630155bd]260            self.updateModel(item.data, item.path)
261
[4992ff2]262        self.model.endResetModel()
[630155bd]263
264    def saveProject(self):
265        """
266        Called when the "Save Project" menu item chosen.
267        """
268        kwargs = {
269            'parent'    : self,
270            'caption'   : 'Save Project',
271            'filter'    : 'Project (*.json)',
[4992ff2]272            'options'   : QtWidgets.QFileDialog.DontUseNativeDialog
[630155bd]273        }
[d6b8a1d]274        name_tuple = QtWidgets.QFileDialog.getSaveFileName(**kwargs)
275        filename = name_tuple[0]
[630155bd]276        if filename:
[d6b8a1d]277            _, extension = os.path.splitext(filename)
278            if not extension:
279                filename = '.'.join((filename, 'json'))
[630155bd]280            self.communicator.statusBarUpdateSignal.emit("Saving Project... %s\n" % os.path.basename(filename))
281            with open(filename, 'w') as outfile:
282                self.manager.save_to_writable(outfile)
283
[5032ea68]284    def deleteFile(self, event):
285        """
286        Delete selected rows from the model
287        """
288        # Assure this is indeed wanted
289        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
290                     "\nDo you want to continue?"
[53c771e]291        reply = QtWidgets.QMessageBox.question(self,
[e540cd2]292                                           'Warning',
293                                           delete_msg,
[53c771e]294                                           QtWidgets.QMessageBox.Yes,
295                                           QtWidgets.QMessageBox.No)
[5032ea68]296
[53c771e]297        if reply == QtWidgets.QMessageBox.No:
[5032ea68]298            return
299
300        # Figure out which rows are checked
301        ind = -1
302        # Use 'while' so the row count is forced at every iteration
[515c23df]303        deleted_items = []
[1420066]304        deleted_names = []
[5032ea68]305        while ind < self.model.rowCount():
306            ind += 1
307            item = self.model.item(ind)
[f0bb711]308
[5032ea68]309            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
310                # Delete these rows from the model
[1420066]311                deleted_names.append(str(self.model.item(ind).text()))
[515c23df]312                deleted_items.append(item)
[f0bb711]313
[5032ea68]314                self.model.removeRow(ind)
315                # Decrement index since we just deleted it
316                ind -= 1
317
[38eb433]318        # Let others know we deleted data
[515c23df]319        self.communicator.dataDeletedSignal.emit(deleted_items)
[f721030]320
[f0bb711]321        # update stored_data
[1420066]322        self.manager.update_stored_data(deleted_names)
[f0bb711]323
[f82ab8c]324    def deleteTheory(self, event):
325        """
326        Delete selected rows from the theory model
327        """
328        # Assure this is indeed wanted
329        delete_msg = "This operation will delete the checked data sets and all the dependents." +\
330                     "\nDo you want to continue?"
[53c771e]331        reply = QtWidgets.QMessageBox.question(self,
[8cb6cd6]332                                           'Warning',
333                                           delete_msg,
[53c771e]334                                           QtWidgets.QMessageBox.Yes,
335                                           QtWidgets.QMessageBox.No)
[f82ab8c]336
[53c771e]337        if reply == QtWidgets.QMessageBox.No:
[f82ab8c]338            return
339
340        # Figure out which rows are checked
341        ind = -1
342        # Use 'while' so the row count is forced at every iteration
343        while ind < self.theory_model.rowCount():
344            ind += 1
345            item = self.theory_model.item(ind)
346            if item and item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
347                # Delete these rows from the model
348                self.theory_model.removeRow(ind)
349                # Decrement index since we just deleted it
350                ind -= 1
351
352        # pass temporarily kept as a breakpoint anchor
353        pass
354
[f721030]355    def sendData(self, event):
356        """
[5032ea68]357        Send selected item data to the current perspective and set the relevant notifiers
[f721030]358        """
[5032ea68]359        # Set the signal handlers
360        self.communicator.updateModelFromPerspectiveSignal.connect(self.updateModelFromPerspective)
[f721030]361
[adf81b8]362        def isItemReady(index):
[5032ea68]363            item = self.model.item(index)
[adf81b8]364            return item.isCheckable() and item.checkState() == QtCore.Qt.Checked
365
366        # Figure out which rows are checked
367        selected_items = [self.model.item(index)
[b3e8629]368                          for index in range(self.model.rowCount())
[adf81b8]369                          if isItemReady(index)]
[f721030]370
[f82ab8c]371        if len(selected_items) < 1:
372            return
373
[f721030]374        # Which perspective has been selected?
[1970780]375        if len(selected_items) > 1 and not self._perspective().allowBatch():
376            msg = self._perspective().title() + " does not allow multiple data."
[53c771e]377            msgbox = QtWidgets.QMessageBox()
378            msgbox.setIcon(QtWidgets.QMessageBox.Critical)
[5032ea68]379            msgbox.setText(msg)
[53c771e]380            msgbox.setStandardButtons(QtWidgets.QMessageBox.Ok)
[5032ea68]381            retval = msgbox.exec_()
382            return
[a281ab8]383
[f721030]384        # Notify the GuiManager about the send request
[ee18d33]385        self._perspective().setData(data_item=selected_items, is_batch=self.chkBatch.isChecked())
[5032ea68]386
[f82ab8c]387    def freezeTheory(self, event):
388        """
389        Freeze selected theory rows.
390
[ca8b853]391        "Freezing" means taking the plottable data from the Theory item
392        and copying it to a separate top-level item in Data.
[f82ab8c]393        """
[ca8b853]394        # Figure out which rows are checked
[f82ab8c]395        # Use 'while' so the row count is forced at every iteration
396        outer_index = -1
[481ff26]397        theories_copied = 0
[ca8b853]398        while outer_index < self.theory_model.rowCount():
[f82ab8c]399            outer_index += 1
[ca8b853]400            outer_item = self.theory_model.item(outer_index)
[f82ab8c]401            if not outer_item:
402                continue
[ca8b853]403            if outer_item.isCheckable() and \
404                   outer_item.checkState() == QtCore.Qt.Checked:
[7969b9c]405                self.model.beginResetModel()
[ca8b853]406                theories_copied += 1
[685e0e3]407                new_item = self.cloneTheory(outer_item)
[ca8b853]408                self.model.appendRow(new_item)
[7969b9c]409                self.model.endResetModel()
[f82ab8c]410
[481ff26]411        freeze_msg = ""
412        if theories_copied == 0:
413            return
414        elif theories_copied == 1:
[ca8b853]415            freeze_msg = "1 theory copied from the Theory tab as a data set"
[481ff26]416        elif theories_copied > 1:
[ca8b853]417            freeze_msg = "%i theories copied from the Theory tab as data sets" % theories_copied
[481ff26]418        else:
419            freeze_msg = "Unexpected number of theories copied: %i" % theories_copied
[b3e8629]420            raise AttributeError(freeze_msg)
[481ff26]421        self.communicator.statusBarUpdateSignal.emit(freeze_msg)
422        # Actively switch tabs
423        self.setCurrentIndex(1)
424
[685e0e3]425    def cloneTheory(self, item_from):
426        """
427        Manually clone theory items into a new HashableItem
428        """
429        new_item = GuiUtils.HashableStandardItem()
430        new_item.setCheckable(True)
431        new_item.setCheckState(QtCore.Qt.Checked)
432        info_item = QtGui.QStandardItem("Info")
433        data_item = QtGui.QStandardItem()
434        data_item.setData(item_from.child(0).data())
435        new_item.setText(item_from.text())
436        new_item.setChild(0, data_item)
437        new_item.setChild(1, info_item)
438        # Append a "unique" descriptor to the name
439        time_bit = str(time.time())[7:-1].replace('.', '')
440        new_name = new_item.text() + '_@' + time_bit
441        new_item.setText(new_name)
442        # Change the underlying data so it is no longer a theory
443        try:
444            new_item.child(0).data().is_data = True
445        except AttributeError:
446            #no data here, pass
447            pass
448        return new_item
449
[481ff26]450    def recursivelyCloneItem(self, item):
451        """
452        Clone QStandardItem() object
453        """
454        new_item = item.clone()
455        # clone doesn't do deepcopy :(
[b3e8629]456        for child_index in range(item.rowCount()):
[481ff26]457            child_item = self.recursivelyCloneItem(item.child(child_index))
458            new_item.setChild(child_index, child_item)
459        return new_item
[f82ab8c]460
[27313b7]461    def updatePlotName(self, name_tuple):
462        """
463        Modify the name of the current plot
464        """
465        old_name, current_name = name_tuple
466        ind = self.cbgraph.findText(old_name)
467        self.cbgraph.setCurrentIndex(ind)
468        self.cbgraph.setItemText(ind, current_name)
469
[7d8bebf]470    def updateGraphCount(self, graph_list):
471        """
472        Modify the graph name combo and potentially remove
473        deleted graphs
474        """
475        self.updateGraphCombo(graph_list)
476
477        if not self.active_plots:
478            return
479        new_plots = [PlotHelper.plotById(plot) for plot in graph_list]
[b3e8629]480        active_plots_copy = list(self.active_plots.keys())
[7d8bebf]481        for plot in active_plots_copy:
482            if self.active_plots[plot] in new_plots:
483                continue
484            self.active_plots.pop(plot)
485
[8cb6cd6]486    def updateGraphCombo(self, graph_list):
487        """
488        Modify Graph combo box on graph add/delete
489        """
490        orig_text = self.cbgraph.currentText()
491        self.cbgraph.clear()
[0268aed]492        self.cbgraph.insertItems(0, graph_list)
[8cb6cd6]493        ind = self.cbgraph.findText(orig_text)
494        if ind > 0:
495            self.cbgraph.setCurrentIndex(ind)
496
[83d6249]497    def updatePerspectiveCombo(self, index):
498        """
499        Notify the gui manager about the new perspective chosen.
500        """
[8ac3551]501        self.communicator.perspectiveChangedSignal.emit(self.cbFitting.itemText(index))
[1970780]502        self.chkBatch.setEnabled(self.parent.perspective().allowBatch())
[83d6249]503
[d4dac80]504    def itemFromFilename(self, filename):
505        """
506        Retrieves model item corresponding to the given filename
507        """
508        item = GuiUtils.itemFromFilename(filename, self.model)
509        return item
510
[3b3b40b]511    def displayFile(self, filename=None, is_data=True):
[d48cc19]512        """
513        Forces display of charts for the given filename
514        """
[3b3b40b]515        model = self.model if is_data else self.theory_model
[88e1f57]516        # Now query the model item for available plots
[d48cc19]517        plots = GuiUtils.plotsFromFilename(filename, model)
[ebfe223]518        ids = [list(self.active_plots.values())[id].data.id for id in range(len(self.active_plots))]
[88e1f57]519
520        new_plots = []
[6ff103a]521        for item, plot in plots.items():
[fef38e8]522            plot_id = plot.id
[ebfe223]523            if plot_id in ids:
[7d8bebf]524                self.active_plots[plot_id].replacePlot(plot_id, plot)
[fef38e8]525            else:
[88e1f57]526                # 'sophisticated' test to generate standalone plot for residuals
527                if 'esiduals' in plot.title:
528                    self.plotData([(item, plot)])
529                else:
530                    new_plots.append((item, plot))
531
532        if new_plots:
533            self.plotData(new_plots)
[56b22f9]534
[3b3b40b]535    def displayData(self, data_list):
536        """
537        Forces display of charts for the given data set
538        """
539        plot_to_show = data_list[0]
540        # passed plot is used ONLY to figure out its title,
541        # so all the charts related by it can be pulled from
542        # the data explorer indices.
543        filename = plot_to_show.filename
544        self.displayFile(filename=filename, is_data=plot_to_show.is_data)
545
[56b22f9]546    def addDataPlot2D(self, plot_set, item):
[672b8ab]547        """
548        Create a new 2D plot and add it to the workspace
549        """
[56b22f9]550        plot2D = Plotter2D(self)
551        plot2D.item = item
552        plot2D.plot(plot_set)
553        self.addPlot(plot2D)
[88e1f57]554        self.active_plots[plot2D.data.id] = plot2D
[56b22f9]555        #============================================
[672b8ab]556        # Experimental hook for silx charts
557        #============================================
[56b22f9]558        ## Attach silx
559        #from silx.gui import qt
560        #from silx.gui.plot import StackView
561        #sv = StackView()
562        #sv.setColormap("jet", autoscale=True)
563        #sv.setStack(plot_set.data.reshape(1,100,100))
564        ##sv.setLabels(["x: -10 to 10 (200 samples)",
565        ##              "y: -10 to 5 (150 samples)"])
566        #sv.show()
567        #============================================
568
569    def plotData(self, plots):
570        """
571        Takes 1D/2D data and generates a single plot (1D) or multiple plots (2D)
[1042dba]572        """
573        # Call show on requested plots
[31c5b58]574        # All same-type charts in one plot
[3bdbfcc]575        for item, plot_set in plots:
[49e124c]576            if isinstance(plot_set, Data1D):
[7d8bebf]577                if not 'new_plot' in locals():
578                    new_plot = Plotter(self)
[d9150d8]579                    new_plot.item = item
[9290b1a]580                new_plot.plot(plot_set)
[88e1f57]581                # active_plots may contain multiple charts
582                self.active_plots[plot_set.id] = new_plot
[49e124c]583            elif isinstance(plot_set, Data2D):
[56b22f9]584                self.addDataPlot2D(plot_set, item)
[49e124c]585            else:
586                msg = "Incorrect data type passed to Plotting"
[b3e8629]587                raise AttributeError(msg)
[49e124c]588
[7d8bebf]589        if 'new_plot' in locals() and \
[b4b8589]590            hasattr(new_plot, 'data') and \
591            isinstance(new_plot.data, Data1D):
[56b22f9]592                self.addPlot(new_plot)
593
594    def newPlot(self):
595        """
596        Select checked data and plot it
597        """
598        # Check which tab is currently active
599        if self.current_view == self.treeView:
600            plots = GuiUtils.plotsFromCheckedItems(self.model)
601        else:
602            plots = GuiUtils.plotsFromCheckedItems(self.theory_model)
603
604        self.plotData(plots)
[1042dba]605
[56b22f9]606    def addPlot(self, new_plot):
[31c5b58]607        """
608        Helper method for plot bookkeeping
609        """
610        # Update the global plot counter
[0268aed]611        title = str(PlotHelper.idOfPlot(new_plot))
[31c5b58]612        new_plot.setWindowTitle(title)
[8cb6cd6]613
[7d8bebf]614        # Set the object name to satisfy the Squish object picker
615        new_plot.setObjectName(title)
616
[31c5b58]617        # Add the plot to the workspace
[d9150d8]618        plot_widget = self.parent.workspace().addSubWindow(new_plot)
[8cb6cd6]619
[31c5b58]620        # Show the plot
621        new_plot.show()
[fbfc488]622        new_plot.canvas.draw()
[f721030]623
[d9150d8]624        # Update the plot widgets dict
625        self.plot_widgets[title]=plot_widget
626
[31c5b58]627        # Update the active chart list
[88e1f57]628        #self.active_plots[new_plot.data.id] = new_plot
[8cb6cd6]629
630    def appendPlot(self):
631        """
632        Add data set(s) to the existing matplotlib chart
633        """
634        # new plot data
635        new_plots = GuiUtils.plotsFromCheckedItems(self.model)
636
637        # old plot data
[0268aed]638        plot_id = str(self.cbgraph.currentText())
[8cb6cd6]639
[0268aed]640        assert plot_id in PlotHelper.currentPlots(), "No such plot: %s"%(plot_id)
[8cb6cd6]641
642        old_plot = PlotHelper.plotById(plot_id)
643
[14d9c7b]644        # Add new data to the old plot, if data type is the same.
[3bdbfcc]645        for _, plot_set in new_plots:
[14d9c7b]646            if type(plot_set) is type(old_plot._data):
[31c5b58]647                old_plot.data = plot_set
[14d9c7b]648                old_plot.plot()
[8cb6cd6]649
[7d077d1]650    def updatePlot(self, new_data):
651        """
652        Modify existing plot for immediate response
653        """
654        data = new_data[0]
655        assert type(data).__name__ in ['Data1D', 'Data2D']
656
657        id = data.id
[b3e8629]658        if data.id in list(self.active_plots.keys()):
[7d077d1]659            self.active_plots[id].replacePlot(id, data)
660
[f721030]661    def chooseFiles(self):
662        """
[5032ea68]663        Shows the Open file dialog and returns the chosen path(s)
[f721030]664        """
665        # List of known extensions
666        wlist = self.getWlist()
667
668        # Location is automatically saved - no need to keep track of the last dir
[5032ea68]669        # But only with Qt built-in dialog (non-platform native)
[4992ff2]670        paths = QtWidgets.QFileDialog.getOpenFileNames(self, "Choose a file", "",
[7969b9c]671                wlist, None, QtWidgets.QFileDialog.DontUseNativeDialog)[0]
[fbfc488]672        if not paths:
[f721030]673            return
674
[0cd8612]675        if not isinstance(paths, list):
[f721030]676            paths = [paths]
677
[9e426c1]678        return paths
[f721030]679
680    def readData(self, path):
681        """
[481ff26]682        verbatim copy-paste from
683           sasgui.guiframe.local_perspectives.data_loader.data_loader.py
[f721030]684        slightly modified for clarity
685        """
686        message = ""
687        log_msg = ''
688        output = {}
689        any_error = False
690        data_error = False
691        error_message = ""
[e540cd2]692        number_of_files = len(path)
693        self.communicator.progressBarUpdateSignal.emit(0.0)
694
695        for index, p_file in enumerate(path):
[f721030]696            basename = os.path.basename(p_file)
697            _, extension = os.path.splitext(basename)
[0cd8612]698            if extension.lower() in GuiUtils.EXTENSIONS:
[f721030]699                any_error = True
700                log_msg = "Data Loader cannot "
701                log_msg += "load: %s\n" % str(p_file)
702                log_msg += """Please try to open that file from "open project" """
703                log_msg += """or "open analysis" menu\n"""
704                error_message = log_msg + "\n"
705                logging.info(log_msg)
706                continue
707
708            try:
[5032ea68]709                message = "Loading Data... " + str(basename) + "\n"
[f721030]710
711                # change this to signal notification in GuiManager
[f82ab8c]712                self.communicator.statusBarUpdateSignal.emit(message)
[f721030]713
714                output_objects = self.loader.load(p_file)
715
716                # Some loaders return a list and some just a single Data1D object.
717                # Standardize.
718                if not isinstance(output_objects, list):
719                    output_objects = [output_objects]
720
721                for item in output_objects:
[481ff26]722                    # cast sascalc.dataloader.data_info.Data1D into
723                    # sasgui.guiframe.dataFitting.Data1D
[f721030]724                    # TODO : Fix it
725                    new_data = self.manager.create_gui_data(item, p_file)
726                    output[new_data.id] = new_data
[481ff26]727
728                    # Model update should be protected
729                    self.mutex.lock()
[f721030]730                    self.updateModel(new_data, p_file)
[7969b9c]731                    #self.model.reset()
732                    QtWidgets.QApplication.processEvents()
[481ff26]733                    self.mutex.unlock()
[f721030]734
735                    if hasattr(item, 'errors'):
736                        for error_data in item.errors:
737                            data_error = True
738                            message += "\tError: {0}\n".format(error_data)
739                    else:
[5032ea68]740
[f721030]741                        logging.error("Loader returned an invalid object:\n %s" % str(item))
742                        data_error = True
743
[5032ea68]744            except Exception as ex:
[b3e8629]745                logging.error(sys.exc_info()[1])
[5032ea68]746
[f721030]747                any_error = True
748            if any_error or error_message != "":
749                if error_message == "":
750                    error = "Error: " + str(sys.exc_info()[1]) + "\n"
751                    error += "while loading Data: \n%s\n" % str(basename)
752                    error_message += "The data file you selected could not be loaded.\n"
753                    error_message += "Make sure the content of your file"
754                    error_message += " is properly formatted.\n\n"
755                    error_message += "When contacting the SasView team, mention the"
756                    error_message += " following:\n%s" % str(error)
757                elif data_error:
758                    base_message = "Errors occurred while loading "
759                    base_message += "{0}\n".format(basename)
760                    base_message += "The data file loaded but with errors.\n"
761                    error_message = base_message + error_message
762                else:
763                    error_message += "%s\n" % str(p_file)
[481ff26]764
[e540cd2]765            current_percentage = int(100.0* index/number_of_files)
766            self.communicator.progressBarUpdateSignal.emit(current_percentage)
767
[f721030]768        if any_error or error_message:
[0cd8612]769            logging.error(error_message)
770            status_bar_message = "Errors occurred while loading %s" % format(basename)
771            self.communicator.statusBarUpdateSignal.emit(status_bar_message)
[f721030]772
773        else:
774            message = "Loading Data Complete! "
775        message += log_msg
[0cd8612]776        # Notify the progress bar that the updates are over.
[e540cd2]777        self.communicator.progressBarUpdateSignal.emit(-1)
[454670d]778        self.communicator.statusBarUpdateSignal.emit(message)
[481ff26]779
[a281ab8]780        return output, message
[f721030]781
782    def getWlist(self):
783        """
[f82ab8c]784        Wildcards of files we know the format of.
[f721030]785        """
786        # Display the Qt Load File module
787        cards = self.loader.get_wildcards()
788
789        # get rid of the wx remnant in wildcards
790        # TODO: modify sasview loader get_wildcards method, after merge,
791        # so this kludge can be avoided
792        new_cards = []
793        for item in cards:
794            new_cards.append(item[:item.find("|")])
795        wlist = ';;'.join(new_cards)
796
797        return wlist
[488c49d]798
799    def selectData(self, index):
800        """
801        Callback method for modifying the TreeView on Selection Options change
802        """
803        if not isinstance(index, int):
804            msg = "Incorrect type passed to DataExplorer.selectData()"
[b3e8629]805            raise AttributeError(msg)
[488c49d]806
807        # Respond appropriately
808        if index == 0:
809            # Select All
[5032ea68]810            for index in range(self.model.rowCount()):
811                item = self.model.item(index)
812                if item.isCheckable() and item.checkState() == QtCore.Qt.Unchecked:
[488c49d]813                    item.setCheckState(QtCore.Qt.Checked)
814        elif index == 1:
815            # De-select All
[5032ea68]816            for index in range(self.model.rowCount()):
817                item = self.model.item(index)
818                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked:
[488c49d]819                    item.setCheckState(QtCore.Qt.Unchecked)
820
821        elif index == 2:
822            # Select All 1-D
[5032ea68]823            for index in range(self.model.rowCount()):
824                item = self.model.item(index)
[488c49d]825                item.setCheckState(QtCore.Qt.Unchecked)
[5032ea68]826
827                try:
[8548d739]828                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
[5032ea68]829                except AttributeError:
830                    msg = "Bad structure of the data model."
[b3e8629]831                    raise RuntimeError(msg)
[5032ea68]832
833                if is1D:
[488c49d]834                    item.setCheckState(QtCore.Qt.Checked)
835
836        elif index == 3:
837            # Unselect All 1-D
[5032ea68]838            for index in range(self.model.rowCount()):
839                item = self.model.item(index)
840
841                try:
[8548d739]842                    is1D = isinstance(GuiUtils.dataFromItem(item), Data1D)
[5032ea68]843                except AttributeError:
844                    msg = "Bad structure of the data model."
[b3e8629]845                    raise RuntimeError(msg)
[5032ea68]846
847                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is1D:
[488c49d]848                    item.setCheckState(QtCore.Qt.Unchecked)
849
850        elif index == 4:
851            # Select All 2-D
[5032ea68]852            for index in range(self.model.rowCount()):
853                item = self.model.item(index)
854                item.setCheckState(QtCore.Qt.Unchecked)
855                try:
[8548d739]856                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
[5032ea68]857                except AttributeError:
858                    msg = "Bad structure of the data model."
[b3e8629]859                    raise RuntimeError(msg)
[5032ea68]860
861                if is2D:
[488c49d]862                    item.setCheckState(QtCore.Qt.Checked)
863
864        elif index == 5:
865            # Unselect All 2-D
[5032ea68]866            for index in range(self.model.rowCount()):
867                item = self.model.item(index)
868
869                try:
[8548d739]870                    is2D = isinstance(GuiUtils.dataFromItem(item), Data2D)
[5032ea68]871                except AttributeError:
872                    msg = "Bad structure of the data model."
[b3e8629]873                    raise RuntimeError(msg)
[5032ea68]874
875                if item.isCheckable() and item.checkState() == QtCore.Qt.Checked and is2D:
[488c49d]876                    item.setCheckState(QtCore.Qt.Unchecked)
877
878        else:
879            msg = "Incorrect value in the Selection Option"
880            # Change this to a proper logging action
[b3e8629]881            raise Exception(msg)
[488c49d]882
[4b71e91]883    def contextMenu(self):
[e540cd2]884        """
[4b71e91]885        Define actions and layout of the right click context menu
[e540cd2]886        """
[4b71e91]887        # Create a custom menu based on actions defined in the UI file
[4992ff2]888        self.context_menu = QtWidgets.QMenu(self)
[4b71e91]889        self.context_menu.addAction(self.actionDataInfo)
890        self.context_menu.addAction(self.actionSaveAs)
891        self.context_menu.addAction(self.actionQuickPlot)
892        self.context_menu.addSeparator()
893        self.context_menu.addAction(self.actionQuick3DPlot)
894        self.context_menu.addAction(self.actionEditMask)
[c6fb57c]895        self.context_menu.addSeparator()
896        self.context_menu.addAction(self.actionDelete)
897
[4b71e91]898
899        # Define the callbacks
900        self.actionDataInfo.triggered.connect(self.showDataInfo)
901        self.actionSaveAs.triggered.connect(self.saveDataAs)
902        self.actionQuickPlot.triggered.connect(self.quickDataPlot)
903        self.actionQuick3DPlot.triggered.connect(self.quickData3DPlot)
904        self.actionEditMask.triggered.connect(self.showEditDataMask)
[c6fb57c]905        self.actionDelete.triggered.connect(self.deleteItem)
[e540cd2]906
907    def onCustomContextMenu(self, position):
908        """
[4b71e91]909        Show the right-click context menu in the data treeview
[e540cd2]910        """
[cbcdd2c]911        index = self.current_view.indexAt(position)
912        proxy = self.current_view.model()
913        model = proxy.sourceModel()
914
[e540cd2]915        if index.isValid():
[cbcdd2c]916            model_item = model.itemFromIndex(proxy.mapToSource(index))
[4b71e91]917            # Find the mapped index
918            orig_index = model_item.isCheckable()
919            if orig_index:
920                # Check the data to enable/disable actions
[8548d739]921                is_2D = isinstance(GuiUtils.dataFromItem(model_item), Data2D)
[4b71e91]922                self.actionQuick3DPlot.setEnabled(is_2D)
923                self.actionEditMask.setEnabled(is_2D)
924                # Fire up the menu
[cbcdd2c]925                self.context_menu.exec_(self.current_view.mapToGlobal(position))
[4b71e91]926
927    def showDataInfo(self):
928        """
929        Show a simple read-only text edit with data information.
930        """
[cbcdd2c]931        index = self.current_view.selectedIndexes()[0]
932        proxy = self.current_view.model()
933        model = proxy.sourceModel()
934        model_item = model.itemFromIndex(proxy.mapToSource(index))
935
[8548d739]936        data = GuiUtils.dataFromItem(model_item)
[28a84e9]937        if isinstance(data, Data1D):
[4b71e91]938            text_to_show = GuiUtils.retrieveData1d(data)
[28a84e9]939            # Hardcoded sizes to enable full width rendering with default font
[4b71e91]940            self.txt_widget.resize(420,600)
941        else:
942            text_to_show = GuiUtils.retrieveData2d(data)
[28a84e9]943            # Hardcoded sizes to enable full width rendering with default font
[4b71e91]944            self.txt_widget.resize(700,600)
945
946        self.txt_widget.setReadOnly(True)
947        self.txt_widget.setWindowFlags(QtCore.Qt.Window)
948        self.txt_widget.setWindowIcon(QtGui.QIcon(":/res/ball.ico"))
949        self.txt_widget.setWindowTitle("Data Info: %s" % data.filename)
[d5c5d3d]950        self.txt_widget.clear()
[4b71e91]951        self.txt_widget.insertPlainText(text_to_show)
952
953        self.txt_widget.show()
[28a84e9]954        # Move the slider all the way up, if present
[4b71e91]955        vertical_scroll_bar = self.txt_widget.verticalScrollBar()
[7969b9c]956        vertical_scroll_bar.triggerAction(QtWidgets.QScrollBar.SliderToMinimum)
[4b71e91]957
958    def saveDataAs(self):
959        """
[28a84e9]960        Save the data points as either txt or xml
[4b71e91]961        """
[cbcdd2c]962        index = self.current_view.selectedIndexes()[0]
963        proxy = self.current_view.model()
964        model = proxy.sourceModel()
965        model_item = model.itemFromIndex(proxy.mapToSource(index))
966
[8548d739]967        data = GuiUtils.dataFromItem(model_item)
[28a84e9]968        if isinstance(data, Data1D):
969            GuiUtils.saveData1D(data)
970        else:
971            GuiUtils.saveData2D(data)
[4b71e91]972
973    def quickDataPlot(self):
[1af348e]974        """
975        Frozen plot - display an image of the plot
976        """
[cbcdd2c]977        index = self.current_view.selectedIndexes()[0]
978        proxy = self.current_view.model()
979        model = proxy.sourceModel()
980        model_item = model.itemFromIndex(proxy.mapToSource(index))
981
[8548d739]982        data = GuiUtils.dataFromItem(model_item)
[39551a68]983
[ef01be4]984        method_name = 'Plotter'
985        if isinstance(data, Data2D):
986            method_name='Plotter2D'
[1af348e]987
[fce6c55]988        self.new_plot = globals()[method_name](self, quickplot=True)
989        self.new_plot.data = data
[5236449]990        #new_plot.plot(marker='o')
[fce6c55]991        self.new_plot.plot()
[39551a68]992
993        # Update the global plot counter
994        title = "Plot " + data.name
[fce6c55]995        self.new_plot.setWindowTitle(title)
[39551a68]996
997        # Show the plot
[fce6c55]998        self.new_plot.show()
[4b71e91]999
1000    def quickData3DPlot(self):
1001        """
[55d89f8]1002        Slowish 3D plot
[4b71e91]1003        """
[cbcdd2c]1004        index = self.current_view.selectedIndexes()[0]
1005        proxy = self.current_view.model()
1006        model = proxy.sourceModel()
1007        model_item = model.itemFromIndex(proxy.mapToSource(index))
1008
[55d89f8]1009        data = GuiUtils.dataFromItem(model_item)
1010
[fce6c55]1011        self.new_plot = Plotter2D(self, quickplot=True, dimension=3)
1012        self.new_plot.data = data
1013        self.new_plot.plot()
[55d89f8]1014
1015        # Update the global plot counter
1016        title = "Plot " + data.name
[fce6c55]1017        self.new_plot.setWindowTitle(title)
[55d89f8]1018
1019        # Show the plot
[fce6c55]1020        self.new_plot.show()
[4b71e91]1021
[e20870bc]1022    def showEditDataMask(self, data=None):
[4b71e91]1023        """
[cad617b]1024        Mask Editor for 2D plots
[4b71e91]1025        """
[e20870bc]1026        if data is None or not isinstance(data, Data2D):
1027            index = self.current_view.selectedIndexes()[0]
1028            proxy = self.current_view.model()
1029            model = proxy.sourceModel()
1030            model_item = model.itemFromIndex(proxy.mapToSource(index))
[cbcdd2c]1031
[e20870bc]1032            data = GuiUtils.dataFromItem(model_item)
[416fa8f]1033
1034        mask_editor = MaskEditor(self, data)
[cad617b]1035        # Modal dialog here.
[416fa8f]1036        mask_editor.exec_()
[f721030]1037
[c6fb57c]1038    def deleteItem(self):
1039        """
1040        Delete the current item
1041        """
1042        # Assure this is indeed wanted
[b1a7a81]1043        delete_msg = "This operation will delete the selected data sets " +\
1044                     "and all the dependents." +\
[c6fb57c]1045                     "\nDo you want to continue?"
1046        reply = QtWidgets.QMessageBox.question(self,
1047                                           'Warning',
1048                                           delete_msg,
1049                                           QtWidgets.QMessageBox.Yes,
1050                                           QtWidgets.QMessageBox.No)
1051
1052        if reply == QtWidgets.QMessageBox.No:
1053            return
1054
[d9150d8]1055        # Every time a row is removed, the indices change, so we'll just remove
1056        # rows and keep calling selectedIndexes until it returns an empty list.
1057        indices = self.current_view.selectedIndexes()
1058
[c6fb57c]1059        proxy = self.current_view.model()
1060        model = proxy.sourceModel()
1061
[515c23df]1062        deleted_items = []
[b1a7a81]1063        deleted_names = []
1064
1065        while len(indices) > 0:
1066            index = indices[0]
[c6fb57c]1067            row_index = proxy.mapToSource(index)
1068            item_to_delete = model.itemFromIndex(row_index)
[cb4d219]1069            if item_to_delete and item_to_delete.isCheckable():
[c6fb57c]1070                row = row_index.row()
[b1a7a81]1071
1072                # store the deleted item details so we can pass them on later
[515c23df]1073                deleted_names.append(item_to_delete.text())
1074                deleted_items.append(item_to_delete)
[b1a7a81]1075
[d9150d8]1076                # Delete corresponding open plots
1077                self.closePlotsForItem(item_to_delete)
1078
[c6fb57c]1079                if item_to_delete.parent():
1080                    # We have a child item - delete from it
1081                    item_to_delete.parent().removeRow(row)
1082                else:
1083                    # delete directly from model
1084                    model.removeRow(row)
[b1a7a81]1085            indices = self.current_view.selectedIndexes()
1086
1087        # Let others know we deleted data
[515c23df]1088        self.communicator.dataDeletedSignal.emit(deleted_items)
[b1a7a81]1089
1090        # update stored_data
1091        self.manager.update_stored_data(deleted_names)
[c6fb57c]1092
[d9150d8]1093    def closePlotsForItem(self, item):
1094        """
1095        Given standard item, close all its currently displayed plots
1096        """
1097        # item - HashableStandardItems of active plots
1098
1099        # {} -> 'Graph1' : HashableStandardItem()
1100        current_plot_items = {}
1101        for plot_name in PlotHelper.currentPlots():
1102            current_plot_items[plot_name] = PlotHelper.plotById(plot_name).item
1103
1104        # item and its hashable children
1105        items_being_deleted = []
1106        if item.rowCount() > 0:
1107            items_being_deleted = [item.child(n) for n in range(item.rowCount())
1108                                   if isinstance(item.child(n), GuiUtils.HashableStandardItem)]
1109        items_being_deleted.append(item)
1110        # Add the parent in case a child is selected
1111        if isinstance(item.parent(), GuiUtils.HashableStandardItem):
1112            items_being_deleted.append(item.parent())
1113
1114        # Compare plot items and items to delete
1115        plots_to_close = set(current_plot_items.values()) & set(items_being_deleted)
1116
1117        for plot_item in plots_to_close:
1118            for plot_name in current_plot_items.keys():
1119                if plot_item == current_plot_items[plot_name]:
1120                    plotter = PlotHelper.plotById(plot_name)
1121                    # try to delete the plot
1122                    try:
1123                        plotter.close()
1124                        #self.parent.workspace().removeSubWindow(plotter)
1125                        self.plot_widgets[plot_name].close()
1126                        self.plot_widgets.pop(plot_name, None)
1127                    except AttributeError as ex:
1128                        logging.error("Closing of %s failed:\n %s" % (plot_name, str(ex)))
1129
1130        pass # debugger anchor
1131
[8ac3551]1132    def onAnalysisUpdate(self, new_perspective=""):
1133        """
1134        Update the perspective combo index based on passed string
1135        """
1136        assert new_perspective in Perspectives.PERSPECTIVES.keys()
1137        self.cbFitting.blockSignals(True)
1138        self.cbFitting.setCurrentIndex(self.cbFitting.findText(new_perspective))
1139        self.cbFitting.blockSignals(False)
1140        pass
1141
[a281ab8]1142    def loadComplete(self, output):
[f721030]1143        """
1144        Post message to status bar and update the data manager
1145        """
[8cb6cd6]1146        assert isinstance(output, tuple)
[e540cd2]1147
[9e426c1]1148        # Reset the model so the view gets updated.
[7969b9c]1149        #self.model.reset()
[e540cd2]1150        self.communicator.progressBarUpdateSignal.emit(-1)
[a281ab8]1151
1152        output_data = output[0]
1153        message = output[1]
[f721030]1154        # Notify the manager of the new data available
[f82ab8c]1155        self.communicator.statusBarUpdateSignal.emit(message)
1156        self.communicator.fileDataReceivedSignal.emit(output_data)
[a281ab8]1157        self.manager.add_data(data_list=output_data)
[f721030]1158
[7969b9c]1159    def loadFailed(self, reason):
[7fb471d]1160        print("File Load Failed with:\n", reason)
1161        pass
1162
[f721030]1163    def updateModel(self, data, p_file):
1164        """
[481ff26]1165        Add data and Info fields to the model item
[f721030]1166        """
1167        # Structure of the model
1168        # checkbox + basename
[481ff26]1169        #     |-------> Data.D object
[f721030]1170        #     |-------> Info
1171        #                 |----> Title:
1172        #                 |----> Run:
1173        #                 |----> Type:
1174        #                 |----> Path:
1175        #                 |----> Process
1176        #                          |-----> process[0].name
[28a84e9]1177        #     |-------> THEORIES
[f721030]1178
1179        # Top-level item: checkbox with label
[6a3e1fe]1180        checkbox_item = GuiUtils.HashableStandardItem()
[f721030]1181        checkbox_item.setCheckable(True)
1182        checkbox_item.setCheckState(QtCore.Qt.Checked)
1183        checkbox_item.setText(os.path.basename(p_file))
1184
1185        # Add the actual Data1D/Data2D object
[6a3e1fe]1186        object_item = GuiUtils.HashableStandardItem()
[b3e8629]1187        object_item.setData(data)
[f721030]1188
[488c49d]1189        checkbox_item.setChild(0, object_item)
1190
[f721030]1191        # Add rows for display in the view
[0cd8612]1192        info_item = GuiUtils.infoFromData(data)
[f721030]1193
[28a84e9]1194        # Set info_item as the first child
[488c49d]1195        checkbox_item.setChild(1, info_item)
[f721030]1196
[28a84e9]1197        # Caption for the theories
1198        checkbox_item.setChild(2, QtGui.QStandardItem("THEORIES"))
1199
[f721030]1200        # New row in the model
[7969b9c]1201        self.model.beginResetModel()
[f721030]1202        self.model.appendRow(checkbox_item)
[7969b9c]1203        self.model.endResetModel()
[481ff26]1204
[5032ea68]1205    def updateModelFromPerspective(self, model_item):
1206        """
[a281ab8]1207        Receive an update model item from a perspective
1208        Make sure it is valid and if so, replace it in the model
[5032ea68]1209        """
[a281ab8]1210        # Assert the correct type
[0cd8612]1211        if not isinstance(model_item, QtGui.QStandardItem):
[5032ea68]1212            msg = "Wrong data type returned from calculations."
[b3e8629]1213            raise AttributeError(msg)
[a281ab8]1214
[1042dba]1215        # TODO: Assert other properties
[a281ab8]1216
[5032ea68]1217        # Reset the view
[7969b9c]1218        ##self.model.reset()
[5032ea68]1219        # Pass acting as a debugger anchor
1220        pass
[481ff26]1221
[5236449]1222    def updateTheoryFromPerspective(self, model_item):
1223        """
1224        Receive an update theory item from a perspective
1225        Make sure it is valid and if so, replace/add in the model
1226        """
1227        # Assert the correct type
1228        if not isinstance(model_item, QtGui.QStandardItem):
1229            msg = "Wrong data type returned from calculations."
[b3e8629]1230            raise AttributeError(msg)
[5236449]1231
1232        # Check if there are any other items for this tab
1233        # If so, delete them
[d6e38661]1234        current_tab_name = model_item.text()
1235        for current_index in range(self.theory_model.rowCount()):
[d6b8a1d]1236            #if current_tab_name in self.theory_model.item(current_index).text():
[d6e38661]1237            if current_tab_name == self.theory_model.item(current_index).text():
1238                return
1239                self.theory_model.removeRow(current_index)
1240                break
[d6b8a1d]1241
[d6e38661]1242        # send in the new item
[5236449]1243        self.theory_model.appendRow(model_item)
1244
Note: See TracBrowser for help on using the repository browser.