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

Last change on this file since c71b20a was c71b20a, checked in by ibressler, 6 years ago

FittingWidget?: showing also new plots on recalc conditionally

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