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

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 2a174d4 was 12db2db6, checked in by Laura Forster <Awork@…>, 6 years ago

Merge branch 'ESS_GUI' of https://github.com/SasView/sasview into ESS_GUI

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