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

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 b4d05bd was b4d05bd, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

allow for only P(Q) or S(Q) to be present in intermediate results; improve comments

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